// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package main import ( "bytes" "encoding/json" "fmt" "go/ast" "go/importer" "go/parser" "go/token" "go/types" "os" "path/filepath" "regexp" "sort" "strings" "time" "github.com/pkg/errors" "github.com/ory/kratos/cmd" "github.com/ory/kratos/text" "github.com/ory/x/clidoc" "github.com/ory/x/clock" ) var ( aSecondAgo = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(-time.Second) inAMinute = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC).Add(time.Minute) // Fixed clocks make the rendered "expired N minutes ago" / "within the next // N minutes" example messages deterministic for doc generation. They are // anchored so the durations come out as exactly one minute. docExpiredClock = clock.NewMock(aSecondAgo.Add(time.Minute)) docUntilClock = clock.NewMock(inAMinute.Add(-time.Minute)) ) var messages map[string]*text.Message func init() { messages = map[string]*text.Message{ "NewInfoNodeLabelVerifyOTP": text.NewInfoNodeLabelVerifyOTP(), "NewInfoNodeLabelVerificationCode": text.NewInfoNodeLabelVerificationCode(), "NewInfoNodeLabelRecoveryCode": text.NewInfoNodeLabelRecoveryCode(), "NewInfoNodeInputPassword": text.NewInfoNodeInputPassword(), "NewInfoNodeInputPhoneNumber": text.NewInfoNodeInputPhoneNumber(), "NewInfoNodeLabelGenerated": text.NewInfoNodeLabelGenerated("{title}", "{name}"), "NewInfoNodeLabelSave": text.NewInfoNodeLabelSave(), "NewInfoNodeLabelSubmit": text.NewInfoNodeLabelSubmit(), "NewInfoNodeLabelID": text.NewInfoNodeLabelID(), "NewErrorValidationSettingsFlowExpired": text.NewErrorValidationSettingsFlowExpired(docExpiredClock, aSecondAgo), "NewInfoSelfServiceSettingsTOTPQRCode": text.NewInfoSelfServiceSettingsTOTPQRCode(), "NewInfoSelfServiceSettingsTOTPSecret": text.NewInfoSelfServiceSettingsTOTPSecret("{secret}"), "NewInfoSelfServiceSettingsTOTPSecretLabel": text.NewInfoSelfServiceSettingsTOTPSecretLabel(), "NewInfoSelfServiceSettingsUpdateSuccess": text.NewInfoSelfServiceSettingsUpdateSuccess(), "NewInfoSelfServiceSettingsUpdateUnlinkTOTP": text.NewInfoSelfServiceSettingsUpdateUnlinkTOTP(), "NewInfoSelfServiceSettingsRevealLookup": text.NewInfoSelfServiceSettingsRevealLookup(), "NewInfoSelfServiceSettingsRegenerateLookup": text.NewInfoSelfServiceSettingsRegenerateLookup(), "NewInfoSelfServiceSettingsDisableLookup": text.NewInfoSelfServiceSettingsDisableLookup(), "NewInfoSelfServiceSettingsLookupConfirm": text.NewInfoSelfServiceSettingsLookupConfirm(), "NewInfoSelfServiceSettingsManagedByOrganization": text.NewInfoSelfServiceSettingsManagedByOrganization(), "NewInfoSelfServiceSettingsLookupSecretList": text.NewInfoSelfServiceSettingsLookupSecretList([]string{"{secrets_list}"}, []interface{}{ text.NewInfoSelfServiceSettingsLookupSecret("{secret}"), text.NewInfoSelfServiceSettingsLookupSecretUsed(aSecondAgo), }), "NewInfoSelfServiceSettingsLookupSecret": text.NewInfoSelfServiceSettingsLookupSecret("{secret}"), "NewInfoSelfServiceSettingsLookupSecretUsed": text.NewInfoSelfServiceSettingsLookupSecretUsed(aSecondAgo), "NewInfoSelfServiceSettingsLookupSecretsLabel": text.NewInfoSelfServiceSettingsLookupSecretsLabel(), "NewInfoSelfServiceSettingsUpdateLinkOIDC": text.NewInfoSelfServiceSettingsUpdateLinkOIDC("{provider}"), "NewInfoSelfServiceSettingsUpdateUnlinkOIDC": text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC("{provider}"), "NewInfoSelfServiceRegisterWebAuthnDisplayName": text.NewInfoSelfServiceRegisterWebAuthnDisplayName(), "NewInfoSelfServiceRemoveWebAuthn": text.NewInfoSelfServiceRemoveWebAuthn("{display_name}", aSecondAgo), "NewInfoSelfServiceRemovePasskey": text.NewInfoSelfServiceRemovePasskey("{display_name}", aSecondAgo), "NewErrorValidationVerificationFlowExpired": text.NewErrorValidationVerificationFlowExpired(docExpiredClock, aSecondAgo), "NewInfoSelfServiceVerificationSuccessful": text.NewInfoSelfServiceVerificationSuccessful(), "NewVerificationEmailSent": text.NewVerificationEmailSent(), "NewVerificationEmailWithCodeSent": text.NewVerificationEmailWithCodeSent(), "NewVerificationPhoneWithCodeSent": text.NewVerificationPhoneWithCodeSent(), "NewInfoSelfServiceVerificationPhoneSuccessful": text.NewInfoSelfServiceVerificationPhoneSuccessful(), "NewErrorValidationVerificationTokenInvalidOrAlreadyUsed": text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed(), "NewErrorValidationVerificationRetrySuccess": text.NewErrorValidationVerificationRetrySuccess(), "NewErrorValidationVerificationStateFailure": text.NewErrorValidationVerificationStateFailure(), "NewErrorValidationVerificationCodeInvalidOrAlreadyUsed": text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), "NewErrorSystemGeneric": text.NewErrorSystemGeneric("{reason}"), "NewErrorSystemNoAuthenticationMethodsAvailable": text.NewErrorSystemNoAuthenticationMethodsAvailable(), "NewErrorSystemOrganizationNoSSOProvidersAvailable": text.NewErrorSystemOrganizationNoSSOProvidersAvailable(), "NewValidationErrorGeneric": text.NewValidationErrorGeneric("{reason}"), "NewValidationErrorRequired": text.NewValidationErrorRequired("{property}"), "NewErrorValidationMinLength": text.NewErrorValidationMinLength(5, 3), "NewErrorValidationMaxLength": text.NewErrorValidationMaxLength(5, 6), "NewErrorValidationInvalidFormat": text.NewErrorValidationInvalidFormat("{pattern}"), "NewErrorValidationMinimum": text.NewErrorValidationMinimum(5, 3), "NewErrorValidationExclusiveMinimum": text.NewErrorValidationExclusiveMinimum(5, 5), "NewErrorValidationMaximum": text.NewErrorValidationMaximum(5, 6), "NewErrorValidationExclusiveMaximum": text.NewErrorValidationExclusiveMaximum(5, 5), "NewErrorValidationMultipleOf": text.NewErrorValidationMultipleOf(7, 3), "NewErrorValidationMaxItems": text.NewErrorValidationMaxItems(3, 4), "NewErrorValidationMinItems": text.NewErrorValidationMinItems(3, 2), "NewErrorValidationUniqueItems": text.NewErrorValidationUniqueItems(0, 2), "NewErrorValidationWrongType": text.NewErrorValidationWrongType([]string{"{allowed_types_list}"}, "{actual_type}"), "NewErrorValidationConst": text.NewErrorValidationConst("{expected}"), "NewErrorValidationConstGeneric": text.NewErrorValidationConstGeneric(), "NewErrorValidationPasswordPolicyViolationGeneric": text.NewErrorValidationPasswordPolicyViolationGeneric("{reason}"), "NewErrorValidationPasswordIdentifierTooSimilar": text.NewErrorValidationPasswordIdentifierTooSimilar(), "NewErrorValidationPasswordMinLength": text.NewErrorValidationPasswordMinLength(6, 5), "NewErrorValidationPasswordMaxLength": text.NewErrorValidationPasswordMaxLength(72, 80), "NewErrorValidationPasswordTooManyBreaches": text.NewErrorValidationPasswordTooManyBreaches(101), "NewErrorValidationPasswordNewSameAsOld": text.NewErrorValidationPasswordNewSameAsOld(), "NewErrorValidationInvalidCredentials": text.NewErrorValidationInvalidCredentials(), "NewErrorValidationDuplicateCredentials": text.NewErrorValidationDuplicateCredentials(), "NewErrorValidationDuplicateCredentialsWithHints": text.NewErrorValidationDuplicateCredentialsWithHints([]string{"{available_credential_types_list}"}, []string{"{available_oidc_providers_list}"}, "{credential_identifier_hint}"), "NewErrorValidationDuplicateCredentialsOnOIDCLink": text.NewErrorValidationDuplicateCredentialsOnOIDCLink(), "NewErrorValidationTOTPVerifierWrong": text.NewErrorValidationTOTPVerifierWrong(), "NewErrorValidationWebAuthnVerifierWrong": text.NewErrorValidationWebAuthnVerifierWrong(), "NewErrorValidationDeviceAuthnVerifierWrong": text.NewErrorValidationDeviceAuthnVerifierWrong(), "NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid": text.NewErrorValidationDeviceAuthnRelaxedAttestationNoLongerValid(), "NewErrorValidationDeviceAuthnKeyReenrollmentRequired": text.NewErrorValidationDeviceAuthnKeyReenrollmentRequired(), "NewErrorValidationLookupAlreadyUsed": text.NewErrorValidationLookupAlreadyUsed(), "NewErrorValidationLookupInvalid": text.NewErrorValidationLookupInvalid(), "NewErrorValidationIdentifierMissing": text.NewErrorValidationIdentifierMissing(), "NewErrorValidationAddressNotVerified": text.NewErrorValidationAddressNotVerified(), "NewErrorValidationNoTOTPDevice": text.NewErrorValidationNoTOTPDevice(), "NewErrorValidationNoLookup": text.NewErrorValidationNoLookup(), "NewErrorValidationNoWebAuthnDevice": text.NewErrorValidationNoWebAuthnDevice(), "NewInfoLoginReAuth": text.NewInfoLoginReAuth(), "NewInfoLoginMFA": text.NewInfoLoginMFA(), "NewInfoLoginTOTPLabel": text.NewInfoLoginTOTPLabel(), "NewInfoLoginLookupLabel": text.NewInfoLoginLookupLabel(), "NewInfoLogin": text.NewInfoLogin(), "NewInfoLoginAndLink": text.NewInfoLoginAndLink(), "NewInfoLoginLinkMessage": text.NewInfoLoginLinkMessage("{duplicateIdentifier}", "{provider}", "{newLoginUrl}", []string{"{available_credential_types_list}"}, []string{"{available_oidc_providers_list}"}), "NewInfoLoginTOTP": text.NewInfoLoginTOTP(), "NewInfoLoginLookup": text.NewInfoLoginLookup(), "NewInfoLoginVerify": text.NewInfoLoginVerify(), "NewInfoLoginWith": text.NewInfoLoginWith("{provider}", "{providerID}"), "NewInfoLoginWithAndLink": text.NewInfoLoginWithAndLink("{provider}"), "NewErrorValidationNoDeviceAuthnDevice": text.NewErrorValidationNoDeviceAuthnDevice(), "NewErrorValidationLoginFlowExpired": text.NewErrorValidationLoginFlowExpired(docExpiredClock, aSecondAgo), "NewErrorValidationLoginNoStrategyFound": text.NewErrorValidationLoginNoStrategyFound(), "NewErrorValidationRegistrationNoStrategyFound": text.NewErrorValidationRegistrationNoStrategyFound(), "NewErrorValidationSettingsNoStrategyFound": text.NewErrorValidationSettingsNoStrategyFound(), "NewErrorValidationRecoveryNoStrategyFound": text.NewErrorValidationRecoveryNoStrategyFound(), "NewErrorValidationVerificationNoStrategyFound": text.NewErrorValidationVerificationNoStrategyFound(), "NewInfoSelfServiceLoginWebAuthn": text.NewInfoSelfServiceLoginWebAuthn(), "NewInfoSelfServiceLoginDeviceAuthn": text.NewInfoSelfServiceLoginDeviceAuthn(), "NewInfoRegistration": text.NewInfoRegistration(), "NewInfoRegistrationWith": text.NewInfoRegistrationWith("{provider}", "{providerID}"), "NewInfoRegistrationContinue": text.NewInfoRegistrationContinue(), "NewInfoRegistrationBack": text.NewInfoRegistrationBack(), "NewInfoSelfServiceChooseCredentials": text.NewInfoSelfServiceChooseCredentials(), "NewErrorValidationRegistrationFlowExpired": text.NewErrorValidationRegistrationFlowExpired(docExpiredClock, aSecondAgo), "NewErrorValidationRecoveryFlowExpired": text.NewErrorValidationRecoveryFlowExpired(docExpiredClock, aSecondAgo), "NewRecoverySuccessful": text.NewRecoverySuccessful(docUntilClock, inAMinute), "NewRecoveryEmailSent": text.NewRecoveryEmailSent(), "NewRecoveryEmailWithCodeSent": text.NewRecoveryEmailWithCodeSent(), "NewRecoveryCodeRecoverySelectAddressSent": text.NewRecoveryCodeRecoverySelectAddressSent("{masked_address}"), "NewRecoveryAskAnyRecoveryAddress": text.NewRecoveryAskAnyRecoveryAddress(), "NewRecoveryAskForFullAddress": text.NewRecoveryAskForFullAddress(), "NewRecoveryAskToChooseAddress": text.NewRecoveryAskToChooseAddress(), "NewRecoveryBack": text.NewRecoveryBack(), "NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed(), "NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed(), "NewErrorValidationRecoveryRetrySuccess": text.NewErrorValidationRecoveryRetrySuccess(), "NewErrorValidationRecoveryStateFailure": text.NewErrorValidationRecoveryStateFailure(), "NewInfoNodeInputEmail": text.NewInfoNodeInputEmail(), "NewInfoNodeInputEmailOrPhone": text.NewInfoNodeInputEmailOrPhone(), "NewInfoNodeResendOTP": text.NewInfoNodeResendOTP(), "NewInfoNodeLoginAndLinkCredential": text.NewInfoNodeLoginAndLinkCredential(), "NewInfoNodeLabelContinue": text.NewInfoNodeLabelContinue(), "NewInfoSelfServiceSettingsRegisterWebAuthn": text.NewInfoSelfServiceSettingsRegisterWebAuthn(), "NewInfoSelfServiceSettingsRegisterPasskey": text.NewInfoSelfServiceSettingsRegisterPasskey(), "NewInfoLoginWebAuthnPasswordless": text.NewInfoLoginWebAuthnPasswordless(), "NewInfoSelfServiceRegistrationRegisterWebAuthn": text.NewInfoSelfServiceRegistrationRegisterWebAuthn(), "NewInfoSelfServiceContinueLoginWebAuthn": text.NewInfoSelfServiceContinueLoginWebAuthn(), "NewInfoSelfServiceLoginPasskey": text.NewInfoSelfServiceLoginPasskey(), "NewInfoSelfServiceRegistrationRegisterPasskey": text.NewInfoSelfServiceRegistrationRegisterPasskey(), "NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(), "NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(), "NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(), "NewLoginCodeSent": text.NewLoginCodeSent(), "NewLoginCodeSentForAuthenticatedUser": text.NewLoginCodeSentForAuthenticatedUser(), "NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed": text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed(), "NewErrorValidationLoginCodeInvalidOrAlreadyUsed": text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed(), "NewErrorValidationNoCodeUser": text.NewErrorValidationNoCodeUser(), "NewInfoNodeLabelRegistrationCode": text.NewInfoNodeLabelRegistrationCode(), "NewInfoNodeLabelLoginCode": text.NewInfoNodeLabelLoginCode(), "NewErrorValidationLoginRetrySuccessful": text.NewErrorValidationLoginRetrySuccessful(), "NewErrorValidationTraitsMismatch": text.NewErrorValidationTraitsMismatch(), "NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(), "NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(), "NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(), "NewErrorValidationLoginLinkedCredentialsDoNotMatch": text.NewErrorValidationLoginLinkedCredentialsDoNotMatch(), "NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(), "NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(), "NewInfoLoginPassword": text.NewInfoLoginPassword(), "NewErrorValidationAccountNotFound": text.NewErrorValidationAccountNotFound(), "NewInfoSelfServiceLoginAAL2CodeAddress": text.NewInfoSelfServiceLoginAAL2CodeAddress("{channel}", "{address}"), "NewErrorCaptchaFailed": text.NewErrorCaptchaFailed(), "NewCaptchaContainerMessage": text.NewCaptchaContainerMessage(), "NewErrorValidationEmail": text.NewErrorValidationEmail("{value}"), "NewErrorValidationPhone": text.NewErrorValidationPhone("{value}"), "NewErrorValidationIdentityDisabled": text.NewErrorValidationIdentityDisabled(), "NewErrorValidationSettingsTooManyAddressChanges": text.NewErrorValidationSettingsTooManyAddressChanges(), } } func main() { if os.Args[1] == "elements" { path, err := filepath.Abs(os.Args[2]) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to determine absolute path for elements generation: %+v", err) os.Exit(1) } if err := generateElements(path); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to generate locales for elements: %+v", err) os.Exit(1) } return } if err := clidoc.Generate(cmd.NewRootCmd(nil, nil), []string{filepath.Join(os.Args[2], "cli")}); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to generate CLI docs: %+v", err) os.Exit(1) } if err := validateAllMessages(filepath.Join(os.Args[1], "text")); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to validate messages: %+v\n", err) os.Exit(1) } sortedMessages := sortMessages() for i := 1; i < len(sortedMessages); i++ { if sortedMessages[i].ID == sortedMessages[i-1].ID { _, _ = fmt.Fprintf(os.Stderr, "Message ID %d is used more than once: %q %q\n", sortedMessages[i].ID, sortedMessages[i].Text, sortedMessages[i-1].Text) os.Exit(1) } } if err := writeMessages(filepath.Join(os.Args[2], "concepts/ui-messages.md"), sortedMessages); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to generate message table: %+v\n", err) os.Exit(1) } if err := writeMessagesJson(filepath.Join(os.Args[2], "concepts/messages.json"), sortedMessages); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to generate messages.json: %+v\n", err) os.Exit(1) } fmt.Println("All files have been generated and updated.") } func codeEncode(in interface{}) string { out, err := json.MarshalIndent(in, "", " ") if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to encode to JSON: %+v", err) os.Exit(1) } return string(out) } func sortMessages() []*text.Message { var toSort []*text.Message for _, m := range messages { toSort = append(toSort, m) } sort.Slice(toSort, func(i, j int) bool { if toSort[i].ID == toSort[j].ID { return toSort[i].Text < toSort[j].Text } return toSort[i].ID < toSort[j].ID }) return toSort } func writeMessages(path string, sortedMessages []*text.Message) error { //nolint:gosec content, err := os.ReadFile(path) // #nosec if err != nil { return err } var w bytes.Buffer for _, m := range sortedMessages { fmt.Fprintf(&w, `###### %s (%d) %s `, m.Text, m.ID, "```json\n"+codeEncode(m)+"\n```") } r := regexp.MustCompile(`(?s)(.*?)`) result := r.ReplaceAllString(string(content), "\n"+w.String()+"\n") f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) // #nosec if err != nil { return err } if _, err := f.WriteString(result); err != nil { return err } if err := f.Close(); err != nil { return err } return nil } func writeMessagesJson(path string, sortedMessages []*text.Message) error { result := codeEncode(sortedMessages) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) // #nosec if err != nil { return err } if _, err := f.WriteString(result); err != nil { return err } if err := f.Close(); err != nil { return err } return nil } func validateAllMessages(path string) error { type message struct { ID, Name string } usedIDs := make([]message, 0, len(messages)) set := token.NewFileSet() packs, err := parser.ParseDir(set, path, nil, 0) if err != nil { return errors.Wrapf(err, "unable to parse text directory") } info := &types.Info{ Defs: make(map[*ast.Ident]types.Object), } //nolint:staticcheck var pack *ast.Package for _, p := range packs { if p.Name == "text" { pack = p break } } allFiles := make([]*ast.File, 0) for fn, f := range pack.Files { if strings.HasSuffix(fn, "/id.go") { allFiles = append(allFiles, f) } } _, err = (&types.Config{Importer: importer.Default()}).Check("text", set, allFiles, info) if err != nil { return err // type error } for _, f := range pack.Files { for _, d := range f.Decls { switch decl := d.(type) { case *ast.FuncDecl: if name := decl.Name.String(); decl.Name.IsExported() && strings.HasPrefix(name, "New") { if _, ok := messages[name]; !ok { return errors.Errorf("expected to find message %s in the list for the documentation generation but could not", name) } } case *ast.GenDecl: if decl.Tok == token.CONST { for _, spec := range decl.Specs { value := spec.(*ast.ValueSpec) // safe because decl.Tok is token.CONST if t, ok := value.Type.(*ast.Ident); ok { if t.Name == "ID" { for _, name := range value.Names { c := info.ObjectOf(name) if c == nil { return errors.Errorf("expected to find const %s in text/id.go", name.Name) } usedIDs = append(usedIDs, message{ ID: c.(*types.Const).Val().ExactString(), Name: name.Name, }) } } } } } } } } sort.Slice(usedIDs, func(i, j int) bool { return usedIDs[i].ID < usedIDs[j].ID }) for i := 1; i < len(usedIDs); i++ { if usedIDs[i].ID == usedIDs[i-1].ID { return errors.Errorf("message ID %s is used more than once: %s %s", usedIDs[i].ID, usedIDs[i].Name, usedIDs[i-1].Name) } } return nil } func generateElements(messageFilePath string) error { // If the file at messageFilePath does not exist, create it f, err := os.OpenFile(messageFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) // #nosec if err != nil { return errors.Wrapf(err, "unable to open or create message file at %s", messageFilePath) } if err := f.Close(); err != nil { return errors.Wrapf(err, "unable to close message file at %s", messageFilePath) } sortedMessages := sortMessages() if err := writeMessagesJson(messageFilePath, sortedMessages); err != nil { return errors.Wrapf(err, "unable to write messages json to %s", messageFilePath) } return nil }