@clossys/writer 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * - F1: an id is `front-door.<state>.<slot>`, where the slot is one of
14
14
  * `title`, `description`, `label`, `primary`, `secondary`, `notice` or
15
- * `alt`. This version ships 63 ids; `FRONT_DOOR_COPY_IDS` is
15
+ * `alt`. This version ships 72 ids; `FRONT_DOOR_COPY_IDS` is
16
16
  * the closed list and `FrontDoorKey` its element union.
17
17
  * - F2: a `{token}` in a default text is a noun from `FRONT_DOOR_NOUNS`
18
18
  * and is declared in the entry's `placeholders`. The nouns are a closed
@@ -34,230 +34,20 @@
34
34
  * consumer's sign-off.
35
35
  */
36
36
  import { resolveCopyRef } from "./resolve.js";
37
- /** All reserved front-door copy ids, in catalog order. */
38
- export const FRONT_DOOR_COPY_IDS = [
39
- "front-door.sign-in.title",
40
- "front-door.sign-in.description",
41
- "front-door.sign-in.label",
42
- "front-door.sign-in.primary",
43
- "front-door.password.title",
44
- "front-door.password.description",
45
- "front-door.password.label",
46
- "front-door.password.primary",
47
- "front-door.password.secondary",
48
- "front-door.identifier-not-found.notice",
49
- "front-door.forgot-password.label",
50
- "front-door.password.notice",
51
- "front-door.sign-in.alt",
52
- "front-door.code.title",
53
- "front-door.code.description",
54
- "front-door.code.label",
55
- "front-door.code.primary",
56
- "front-door.code.secondary",
57
- "front-door.code.notice",
58
- "front-door.unavailable.notice",
59
- "front-door.rate-limited.notice",
60
- "front-door.locked.notice",
61
- "front-door.expired.notice",
62
- "front-door.signed-out.notice",
63
- "front-door.error.title",
64
- "front-door.error.description",
65
- "front-door.error.primary",
66
- "front-door.not-found.title",
67
- "front-door.not-found.description",
68
- "front-door.not-found.primary",
69
- "front-door.not-authorized.title",
70
- "front-door.not-authorized.description",
71
- "front-door.not-authorized.primary",
72
- "front-door.not-authorized.secondary",
73
- "front-door.access-pending.title",
74
- "front-door.access-pending.description",
75
- "front-door.access-pending.primary",
76
- "front-door.service-unavailable.title",
77
- "front-door.service-unavailable.description",
78
- "front-door.service-unavailable.primary",
79
- "front-door.request-access.description",
80
- "front-door.request-access.label",
81
- "front-door.reset.title",
82
- "front-door.reset.description",
83
- "front-door.reset.label",
84
- "front-door.reset.primary",
85
- "front-door.reset.secondary",
86
- "front-door.reset.notice",
87
- "front-door.activation.title",
88
- "front-door.activation.description",
89
- "front-door.activation.primary",
90
- "front-door.activation.notice",
91
- "front-door.internal-note.label",
92
- "front-door.identifier-required.notice",
93
- "front-door.password-required.notice",
94
- "front-door.code-required.notice",
95
- "front-door.network.notice",
96
- "front-door.password-weak.notice",
97
- "front-door.reset-code.primary",
98
- "front-door.activation.label",
99
- "front-door.activation-first-name.label",
100
- "front-door.activation-last-name.label",
101
- "front-door.name-required.notice",
102
- ];
103
- /** The closed set of nouns a front-door text may name as a `{token}`. */
104
- export const FRONT_DOOR_NOUNS = ["brand", "surface", "identifier", "digest", "requestAccessLabel"];
105
- const DEFAULTS = {
106
- "front-door.sign-in.title": { text: "Sign in", context: "sign-in page: heading" },
107
- "front-door.sign-in.description": { text: "Continue to {surface}.", context: "sign-in page: line under the heading" },
108
- "front-door.sign-in.label": { text: "Email", context: "sign-in page: email field label" },
109
- "front-door.sign-in.primary": { text: "Continue", context: "sign-in page: main button" },
110
- "front-door.password.title": { text: "Enter your password", context: "password page: heading" },
111
- "front-door.password.description": { text: "Signing in as {identifier}.", context: "password page: line under the heading" },
112
- "front-door.password.label": { text: "Password", context: "password page: password field label" },
113
- "front-door.password.primary": { text: "Sign in", context: "password page: main button" },
114
- "front-door.password.secondary": { text: "Use a different email", context: "password page: link back to the email step" },
115
- "front-door.identifier-not-found.notice": {
116
- text: "We couldn’t find an account for that email. Check it and try again.",
117
- context: "sign-in page: notice when no account matches the email",
118
- },
119
- "front-door.forgot-password.label": { text: "Forgot password?", context: "sign-in page: link to start a password reset" },
120
- "front-door.password.notice": { text: "That password isn’t right. Try again or reset it.", context: "password page: notice when the password is wrong" },
121
- "front-door.sign-in.alt": { text: "Sign in to {brand}", context: "sign-in page: alt text for the brand mark" },
122
- "front-door.code.title": { text: "Check your email", context: "code page: heading" },
123
- "front-door.code.description": { text: "Enter the code sent to {identifier}.", context: "code page: line under the heading" },
124
- "front-door.code.label": { text: "Code", context: "code page: code field label" },
125
- "front-door.code.primary": { text: "Verify", context: "code page: main button" },
126
- "front-door.code.secondary": { text: "Send a new code", context: "code page: link to request another code" },
127
- "front-door.code.notice": { text: "That code isn’t right or has expired. Try again or send a new code.", context: "code page: notice when the code is wrong or expired" },
128
- "front-door.unavailable.notice": { text: "Sign-in isn’t available right now. Try again in a few minutes.", context: "sign-in page: notice when sign-in is temporarily unavailable" },
129
- "front-door.rate-limited.notice": { text: "Too many attempts. Wait a few minutes, then try again.", context: "sign-in page: notice when attempts are rate limited" },
130
- "front-door.locked.notice": { text: "This account is temporarily locked after too many attempts. Try again later.", context: "sign-in page: notice when the account is temporarily locked" },
131
- "front-door.expired.notice": { text: "Your session expired. Sign in again to continue.", context: "sign-in page: notice when the session has expired" },
132
- "front-door.signed-out.notice": { text: "You’re signed out.", context: "sign-in page: notice after signing out" },
133
- "front-door.error.title": { text: "This page didn’t load", context: "error page: heading" },
134
- "front-door.error.description": { text: "Something went wrong. Error: {digest}.", context: "error page: line under the heading, with the error digest" },
135
- "front-door.error.primary": { text: "Try again", context: "error page: main button" },
136
- "front-door.not-found.title": { text: "Page not found", context: "not-found page: heading" },
137
- "front-door.not-found.description": { text: "This page doesn’t exist or has moved.", context: "not-found page: line under the heading" },
138
- "front-door.not-found.primary": { text: "Go to {surface}", context: "not-found page: main button back to the surface" },
139
- "front-door.not-authorized.title": { text: "You don’t have access", context: "not-authorized page: heading" },
140
- "front-door.not-authorized.description": { text: "{identifier} doesn’t have access to {surface}.", context: "not-authorized page: line under the heading" },
141
- "front-door.not-authorized.primary": { text: "Switch account", context: "not-authorized page: main button" },
142
- "front-door.not-authorized.secondary": { text: "Sign out", context: "not-authorized page: link to sign out" },
143
- "front-door.access-pending.title": { text: "Access pending", context: "access-pending page: heading" },
144
- "front-door.access-pending.description": { text: "Your access to {surface} awaits approval. Check back later.", context: "access-pending page: line under the heading" },
145
- "front-door.access-pending.primary": { text: "Sign out", context: "access-pending page: main button" },
146
- "front-door.service-unavailable.title": { text: "{surface} isn’t available", context: "service-unavailable page: heading" },
147
- "front-door.service-unavailable.description": { text: "This is temporary. Try again soon.", context: "service-unavailable page: line under the heading" },
148
- "front-door.service-unavailable.primary": { text: "Try again", context: "service-unavailable page: main button" },
149
- "front-door.request-access.description": { text: "Don’t have an account?", context: "sign-in page: line before the request-access link" },
150
- "front-door.request-access.label": { text: "{requestAccessLabel}", context: "sign-in page: request-access link label" },
151
- "front-door.reset.title": { text: "Reset your password", context: "reset page: heading" },
152
- "front-door.reset.description": { text: "Enter your email and we’ll send you a code.", context: "reset page: line under the heading" },
153
- "front-door.reset.label": { text: "New password", context: "reset page: new password field label" },
154
- "front-door.reset.primary": { text: "Send code", context: "reset page: main button" },
155
- "front-door.reset.secondary": { text: "Back to sign in", context: "reset page: link back to sign-in" },
156
- "front-door.reset.notice": { text: "Password updated. Sign in with your new password.", context: "sign-in page: notice after a password reset" },
157
- "front-door.activation.title": { text: "Set up your account", context: "activation page: heading" },
158
- "front-door.activation.description": { text: "You’re invited to {surface}. Choose a password to finish.", context: "activation page: line under the heading" },
159
- "front-door.activation.primary": { text: "Activate account", context: "activation page: main button" },
160
- "front-door.activation.notice": { text: "This invitation has expired or was already used. Ask for a new invitation.", context: "activation page: notice when the invitation is no longer valid" },
161
- "front-door.internal-note.label": { text: "Internal", context: "label marking a note visible only inside the organization" },
162
- "front-door.identifier-required.notice": { text: "Enter your email.", context: "sign-in page: notice when the email is empty" },
163
- "front-door.password-required.notice": { text: "Enter your password.", context: "password page: notice when the password is empty" },
164
- "front-door.code-required.notice": { text: "Enter the code.", context: "code page: notice when the code is empty" },
165
- "front-door.network.notice": { text: "Couldn’t reach {surface}. Check your connection and try again.", context: "any page: notice when the request could not reach the surface" },
166
- "front-door.password-weak.notice": {
167
- text: "That password isn’t strong enough. Use a longer one you haven’t used anywhere else.",
168
- context: "reset and activation pages: notice when the new password is refused as too weak",
169
- },
170
- "front-door.reset-code.primary": { text: "Reset password", context: "reset page, code step: main button" },
171
- "front-door.activation.label": { text: "Password", context: "activation page: password field label" },
172
- "front-door.activation-first-name.label": { text: "First name", context: "activation page: first name field label" },
173
- "front-door.activation-last-name.label": { text: "Last name", context: "activation page: last name field label" },
174
- "front-door.name-required.notice": { text: "Enter your first and last name.", context: "activation page: notice when a name field is empty" },
175
- };
176
- function placeholdersOf(text) {
177
- return [...new Set([...text.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]))];
178
- }
179
- function entryFor(id) {
180
- const { text, context } = DEFAULTS[id];
181
- const placeholders = placeholdersOf(text);
182
- return {
183
- id,
184
- text,
185
- context,
186
- status: "approved",
187
- ...(placeholders.length > 0 ? { placeholders } : {}),
188
- };
189
- }
190
- /** The shipped English defaults for every id in `FRONT_DOOR_COPY_IDS`. */
191
- export const FRONT_DOOR_COPY_EN = {
192
- id: "front-door",
193
- locale: "en",
194
- revision: "1",
195
- source: { kind: "imported", reference: "@clossys/writer/front-door.en.json" },
196
- entries: FRONT_DOOR_COPY_IDS.map(entryFor),
197
- };
198
- /** Whether `value` is one of the reserved front-door copy ids. */
37
+ import { CANONICAL_IDS, CANONICAL_NOUNS, createDefaultCatalog, prepareFrontDoorCopy } from "./front-door-core.js";
38
+ /** Independent mutable legacy catalog objects; private browser defaults never share them. */
39
+ export const FRONT_DOOR_COPY_IDS = [...CANONICAL_IDS];
40
+ export const FRONT_DOOR_NOUNS = [...CANONICAL_NOUNS];
41
+ export const FRONT_DOOR_COPY_EN = createDefaultCatalog();
199
42
  export function isFrontDoorCopyId(value) {
200
43
  return typeof value === "string" && FRONT_DOOR_COPY_IDS.includes(value);
201
44
  }
202
- /** Copies the own, enumerable string-keyed values of `nouns`; anything unreadable reads as no nouns at all. */
203
- function readNouns(nouns) {
204
- const read = new Map();
205
- if (typeof nouns !== "object" || nouns === null || Array.isArray(nouns))
206
- return read;
207
- try {
208
- for (const name of Object.keys(nouns))
209
- read.set(name, nouns[name]);
210
- }
211
- catch {
212
- return new Map();
213
- }
214
- return read;
215
- }
216
- /** Names a lookup key for a message; a key that cannot be stringified (a null-prototype object, a throwing `toString`) reads as its type. */
217
- function describeKey(key) {
218
- try {
219
- return String(key);
220
- }
221
- catch {
222
- return `<${typeof key}>`;
223
- }
224
- }
225
- /**
226
- * Resolves one front-door entry from the shipped English defaults, naming
227
- * the nouns it declares. Only nouns the entry declares reach
228
- * `resolveCopyRef`; a name outside `FRONT_DOOR_NOUNS` is reported as
229
- * `"unknown-noun"` and a declared noun that is absent or blank as
230
- * `"missing-noun"`. An id outside `FRONT_DOOR_COPY_IDS` is
231
- * `"unknown-copy-id"`. Pure and total: the defaults and `nouns` are not
232
- * modified and no input makes this throw.
233
- */
45
+ /** Resolves through the strict registry resolver, preserving approval refusals. */
234
46
  export function resolveFrontDoorCopy(key, nouns) {
235
- if (!isFrontDoorCopyId(key)) {
236
- return {
237
- complete: false,
238
- issues: [{ reason: "unknown-copy-id", id: describeKey(key), message: `Front-door copy id "${describeKey(key)}" is not a reserved id.` }],
239
- };
240
- }
241
- const supplied = readNouns(nouns);
242
- const issues = [];
243
- for (const name of supplied.keys()) {
244
- if (!FRONT_DOOR_NOUNS.includes(name)) {
245
- issues.push({ reason: "unknown-noun", noun: name, message: `"${name}" is not a front-door noun.` });
246
- }
247
- }
248
- const entry = FRONT_DOOR_COPY_EN.entries.find((candidate) => candidate.id === key);
249
- const values = {};
250
- for (const name of entry.placeholders ?? []) {
251
- const value = supplied.get(name);
252
- if (typeof value !== "string" || value.trim().length === 0) {
253
- issues.push({ reason: "missing-noun", id: key, noun: name, message: `Front-door copy "${key}" needs the noun "${name}", which is absent or blank.` });
254
- }
255
- else {
256
- values[name] = value;
257
- }
258
- }
259
- if (issues.length > 0)
260
- return { complete: false, issues };
47
+ const prepared = prepareFrontDoorCopy(key, nouns, FRONT_DOOR_COPY_EN, FRONT_DOOR_COPY_IDS, FRONT_DOOR_NOUNS);
48
+ if (!prepared.complete)
49
+ return prepared;
50
+ const { values } = prepared;
261
51
  const result = resolveCopyRef(FRONT_DOOR_COPY_EN, { id: key, values });
262
52
  if (!result.complete || !result.resolution) {
263
53
  // Unreachable for the shipped catalog (every id resolves, which the tests pin); kept so nothing here can throw or pass silently.
@@ -1 +1 @@
1
- {"version":3,"file":"front-door.js","sourceRoot":"","sources":["../src/front-door.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"front-door.js","sourceRoot":"","sources":["../src/front-door.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAIlH,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,aAAa,CAAoC,CAAC;AACzF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,eAAe,CAAsC,CAAC;AAC1F,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,EAAE,CAAC;AAEzD,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,mBAAyC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACjG,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,oBAAoB,CAAC,GAAiB,EAAE,KAAqB;IAC3E,MAAM,QAAQ,GAAG,oBAAoB,CAAC,GAAG,EAAE,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,CAAC,CAAC;IAC7G,IAAI,CAAC,QAAQ,CAAC,QAAQ;QAAE,OAAO,QAAQ,CAAC;IACxC,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAC;IAC5B,MAAM,MAAM,GAAG,cAAc,CAAC,kBAAkB,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;IACvE,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QAC3C,iIAAiI;QACjI,OAAO;YACL,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,oBAAoB,GAAG,0BAA0B,EAAE,CAAC;SAC1I,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AACrG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clossys/writer",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,11 @@
17
17
  "import": "./dist/voice/index.js"
18
18
  },
19
19
  "./voice-record.template.jsonc": "./templates/voice-record.template.jsonc",
20
- "./front-door.en.json": "./templates/front-door.en.json"
20
+ "./front-door.en.json": "./templates/front-door.en.json",
21
+ "./front-door": {
22
+ "types": "./dist/front-door-browser.d.ts",
23
+ "import": "./dist/front-door-browser.js"
24
+ }
21
25
  },
22
26
  "bin": {
23
27
  "writer-check": "dist/cli.js",
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Immutable shipped English defaults for browser rendering. This entry accepts
3
+ * only a reserved key and nouns; package defaults do not authorize consumer copy.
4
+ * Consumer registries and approval records belong to the strict root resolver.
5
+ */
6
+ import { CANONICAL_IDS, CANONICAL_NOUNS, createDefaultCatalog, prepareFrontDoorCopy } from "./front-door-core.js";
7
+ import type { FrontDoorKey, FrontDoorNouns, FrontDoorCopyResolution } from "./front-door-core.js";
8
+ export type { FrontDoorKey, FrontDoorNoun, FrontDoorNouns, FrontDoorCopyIssueReason, FrontDoorCopyIssue, FrontDoorCopyResolution } from "./front-door-core.js";
9
+
10
+ const defaults = createDefaultCatalog();
11
+ for (const entry of defaults.entries) {
12
+ if (entry.placeholders) Object.freeze(entry.placeholders);
13
+ Object.freeze(entry);
14
+ }
15
+ Object.freeze(defaults.entries);
16
+ Object.freeze(defaults.source);
17
+ Object.freeze(defaults);
18
+
19
+ /** Pure and total; each success returns independent values and provenance. */
20
+ export function resolveFrontDoorCopy(key: FrontDoorKey, nouns: FrontDoorNouns): FrontDoorCopyResolution {
21
+ const prepared = prepareFrontDoorCopy(key, nouns, defaults, CANONICAL_IDS, CANONICAL_NOUNS);
22
+ if (!prepared.complete) return prepared;
23
+ const { entry, values } = prepared;
24
+ const text = entry.text.replace(/\{([^{}]+)\}/g, (_match, name: string) => String(values[name]!));
25
+ return {
26
+ complete: true, text, issues: [], resolution: {
27
+ ref: { id: key, values }, text, recordId: defaults.id, revision: defaults.revision,
28
+ locale: defaults.locale, source: { ...defaults.source! }, entryId: entry.id,
29
+ }
30
+ };
31
+ }
@@ -0,0 +1,283 @@
1
+ /** Pure private catalog and noun validation shared by the two public entry points. */
2
+ import type { CopyRegistry, CopyRegistryEntry, CopyResolution } from "./types.js";
3
+ /** All reserved front-door copy ids, in catalog order. */
4
+ export const CANONICAL_IDS = Object.freeze([
5
+ "front-door.sign-in.title",
6
+ "front-door.sign-in.description",
7
+ "front-door.sign-in.label",
8
+ "front-door.sign-in.primary",
9
+ "front-door.password.title",
10
+ "front-door.password.description",
11
+ "front-door.password.label",
12
+ "front-door.password.primary",
13
+ "front-door.password.secondary",
14
+ "front-door.identifier-not-found.notice",
15
+ "front-door.forgot-password.label",
16
+ "front-door.password.notice",
17
+ "front-door.sign-in.alt",
18
+ "front-door.code.title",
19
+ "front-door.code.description",
20
+ "front-door.code.label",
21
+ "front-door.code.primary",
22
+ "front-door.code.secondary",
23
+ "front-door.code.notice",
24
+ "front-door.unavailable.notice",
25
+ "front-door.rate-limited.notice",
26
+ "front-door.locked.notice",
27
+ "front-door.expired.notice",
28
+ "front-door.signed-out.notice",
29
+ "front-door.error.title",
30
+ "front-door.error.description",
31
+ "front-door.error.primary",
32
+ "front-door.not-found.title",
33
+ "front-door.not-found.description",
34
+ "front-door.not-found.primary",
35
+ "front-door.not-authorized.title",
36
+ "front-door.not-authorized.description",
37
+ "front-door.not-authorized.primary",
38
+ "front-door.not-authorized.secondary",
39
+ "front-door.access-pending.title",
40
+ "front-door.access-pending.description",
41
+ "front-door.access-pending.primary",
42
+ "front-door.service-unavailable.title",
43
+ "front-door.service-unavailable.description",
44
+ "front-door.service-unavailable.primary",
45
+ "front-door.request-access.description",
46
+ "front-door.request-access.label",
47
+ "front-door.reset.title",
48
+ "front-door.reset.description",
49
+ "front-door.reset.label",
50
+ "front-door.reset.primary",
51
+ "front-door.reset.secondary",
52
+ "front-door.reset.notice",
53
+ "front-door.activation.title",
54
+ "front-door.activation.description",
55
+ "front-door.activation.primary",
56
+ "front-door.activation.notice",
57
+ "front-door.internal-note.label",
58
+ "front-door.identifier-required.notice",
59
+ "front-door.password-required.notice",
60
+ "front-door.code-required.notice",
61
+ "front-door.network.notice",
62
+ "front-door.password-weak.notice",
63
+ "front-door.reset-code.primary",
64
+ "front-door.activation.label",
65
+ "front-door.activation-first-name.label",
66
+ "front-door.activation-last-name.label",
67
+ "front-door.name-required.notice",
68
+ "front-door.sign-in-staff-only.description",
69
+ "front-door.sign-in-invite-only.description",
70
+ "front-door.not-authorized-staff-only.title",
71
+ "front-door.not-authorized-staff-only.description",
72
+ "front-door.not-authorized-staff-only.primary",
73
+ "front-door.not-authorized-invite-only.title",
74
+ "front-door.not-authorized-invite-only.description",
75
+ "front-door.not-authorized-invite-only.primary",
76
+ "front-door.identifier-not-found-neutral.notice",
77
+ ] as const);
78
+
79
+ /** A reserved front-door copy id. */
80
+ export type FrontDoorKey = (typeof CANONICAL_IDS)[number];
81
+
82
+ /** The closed set of nouns a front-door text may name as a `{token}`. */
83
+ export const CANONICAL_NOUNS = Object.freeze(["brand", "surface", "identifier", "digest", "requestAccessLabel"] as const);
84
+
85
+ /** One noun a front-door text may name. */
86
+ export type FrontDoorNoun = (typeof CANONICAL_NOUNS)[number];
87
+
88
+ /** The nouns a caller supplies; each is read only when the entry declares it. */
89
+ export type FrontDoorNouns = Partial<Record<FrontDoorNoun, string>>;
90
+
91
+ /** Why a front-door entry could not be resolved. */
92
+ export type FrontDoorCopyIssueReason = "unknown-copy-id" | "missing-noun" | "unknown-noun";
93
+
94
+ /** One reason a front-door entry could not be resolved. */
95
+ export interface FrontDoorCopyIssue {
96
+ reason: FrontDoorCopyIssueReason;
97
+ message: string;
98
+ /** The id asked for, on `"unknown-copy-id"`. */
99
+ id?: string;
100
+ /** The noun at fault, on `"missing-noun"` and `"unknown-noun"`. */
101
+ noun?: string;
102
+ }
103
+
104
+ /**
105
+ * The outcome of `resolveFrontDoorCopy`. `text` and `resolution` are
106
+ * present only when `complete` is `true`; `issues` is empty exactly then.
107
+ */
108
+ export interface FrontDoorCopyResolution {
109
+ complete: boolean;
110
+ text?: string;
111
+ resolution?: CopyResolution;
112
+ issues: FrontDoorCopyIssue[];
113
+ }
114
+
115
+ const DEFAULTS: Readonly<Record<FrontDoorKey, { text: string; context: string }>> = {
116
+ "front-door.sign-in.title": { text: "Sign in", context: "sign-in page: heading" },
117
+ "front-door.sign-in.description": { text: "Continue to {surface}.", context: "sign-in page: line under the heading" },
118
+ "front-door.sign-in.label": { text: "Email", context: "sign-in page: email field label" },
119
+ "front-door.sign-in.primary": { text: "Continue", context: "sign-in page: main button" },
120
+ "front-door.password.title": { text: "Enter your password", context: "password page: heading" },
121
+ "front-door.password.description": { text: "Signing in as {identifier}.", context: "password page: line under the heading" },
122
+ "front-door.password.label": { text: "Password", context: "password page: password field label" },
123
+ "front-door.password.primary": { text: "Sign in", context: "password page: main button" },
124
+ "front-door.password.secondary": { text: "Use a different email", context: "password page: link back to the email step" },
125
+ "front-door.identifier-not-found.notice": {
126
+ text: "We couldn’t find an account for that email. Check it and try again.",
127
+ context: "sign-in page: notice when no account matches the email",
128
+ },
129
+ "front-door.forgot-password.label": { text: "Forgot password?", context: "sign-in page: link to start a password reset" },
130
+ "front-door.password.notice": { text: "That password isn’t right. Try again.", context: "password page: notice when the password is wrong" },
131
+ "front-door.sign-in.alt": { text: "Sign in to {brand}", context: "sign-in page: alt text for the brand mark" },
132
+ "front-door.code.title": { text: "Verify your sign-in", context: "code page: heading" },
133
+ "front-door.code.description": { text: "Enter your verification code to continue.", context: "code page: line under the heading" },
134
+ "front-door.code.label": { text: "Code", context: "code page: code field label" },
135
+ "front-door.code.primary": { text: "Verify", context: "code page: main button" },
136
+ "front-door.code.secondary": { text: "Send a new code", context: "code page: link to request another code" },
137
+ "front-door.code.notice": { text: "That code isn’t right or has expired. Try again.", context: "code page: notice when the code is wrong or expired" },
138
+ "front-door.unavailable.notice": { text: "Sign-in isn’t available right now. Try again in a few minutes.", context: "sign-in page: notice when sign-in is temporarily unavailable" },
139
+ "front-door.rate-limited.notice": { text: "Too many attempts. Wait a few minutes, then try again.", context: "sign-in page: notice when attempts are rate limited" },
140
+ "front-door.locked.notice": { text: "This account is temporarily locked after too many attempts. Try again later.", context: "sign-in page: notice when the account is temporarily locked" },
141
+ "front-door.expired.notice": { text: "Your session expired. Sign in again to continue.", context: "sign-in page: notice when the session has expired" },
142
+ "front-door.signed-out.notice": { text: "You’re signed out.", context: "sign-in page: notice after signing out" },
143
+ "front-door.error.title": { text: "This page didn’t load", context: "error page: heading" },
144
+ "front-door.error.description": { text: "Something went wrong. Error: {digest}.", context: "error page: line under the heading, with the error digest" },
145
+ "front-door.error.primary": { text: "Try again", context: "error page: main button" },
146
+ "front-door.not-found.title": { text: "Page not found", context: "not-found page: heading" },
147
+ "front-door.not-found.description": { text: "This page doesn’t exist or has moved.", context: "not-found page: line under the heading" },
148
+ "front-door.not-found.primary": { text: "Go to {surface}", context: "not-found page: main button back to the surface" },
149
+ "front-door.not-authorized.title": { text: "You don’t have access", context: "not-authorized page: heading" },
150
+ "front-door.not-authorized.description": { text: "{identifier} doesn’t have access to {surface}.", context: "not-authorized page: line under the heading" },
151
+ "front-door.not-authorized.primary": { text: "Switch account", context: "not-authorized page: main button" },
152
+ "front-door.not-authorized.secondary": { text: "Sign out", context: "not-authorized page: link to sign out" },
153
+ "front-door.access-pending.title": { text: "Access pending", context: "access-pending page: heading" },
154
+ "front-door.access-pending.description": { text: "Your access to {surface} awaits approval. Check back later.", context: "access-pending page: line under the heading" },
155
+ "front-door.access-pending.primary": { text: "Sign out", context: "access-pending page: main button" },
156
+ "front-door.service-unavailable.title": { text: "{surface} isn’t available", context: "service-unavailable page: heading" },
157
+ "front-door.service-unavailable.description": { text: "This is temporary. Try again soon.", context: "service-unavailable page: line under the heading" },
158
+ "front-door.service-unavailable.primary": { text: "Try again", context: "service-unavailable page: main button" },
159
+ "front-door.request-access.description": { text: "Don’t have an account?", context: "sign-in page: line before the request-access link" },
160
+ "front-door.request-access.label": { text: "{requestAccessLabel}", context: "sign-in page: request-access link label" },
161
+ "front-door.reset.title": { text: "Reset your password", context: "reset page: heading" },
162
+ "front-door.reset.description": { text: "Enter your email and we’ll send you a code.", context: "reset page: line under the heading" },
163
+ "front-door.reset.label": { text: "New password", context: "reset page: new password field label" },
164
+ "front-door.reset.primary": { text: "Send code", context: "reset page: main button" },
165
+ "front-door.reset.secondary": { text: "Back to sign in", context: "reset page: link back to sign-in" },
166
+ "front-door.reset.notice": { text: "Password updated. Sign in with your new password.", context: "sign-in page: notice after a password reset" },
167
+ "front-door.activation.title": { text: "Set up your account", context: "activation page: heading" },
168
+ "front-door.activation.description": { text: "You’re invited to {surface}. Choose a password to finish.", context: "activation page: line under the heading" },
169
+ "front-door.activation.primary": { text: "Activate account", context: "activation page: main button" },
170
+ "front-door.activation.notice": { text: "This invitation has expired or was already used. Ask for a new invitation.", context: "activation page: notice when the invitation is no longer valid" },
171
+ "front-door.internal-note.label": { text: "Internal", context: "label marking a note visible only inside the organization" },
172
+ "front-door.identifier-required.notice": { text: "Enter your email.", context: "sign-in page: notice when the email is empty" },
173
+ "front-door.password-required.notice": { text: "Enter your password.", context: "password page: notice when the password is empty" },
174
+ "front-door.code-required.notice": { text: "Enter the code.", context: "code page: notice when the code is empty" },
175
+ "front-door.network.notice": { text: "Couldn’t reach {surface}. Check your connection and try again.", context: "any page: notice when the request could not reach the surface" },
176
+ "front-door.password-weak.notice": {
177
+ text: "That password isn’t strong enough. Use a longer one you haven’t used anywhere else.",
178
+ context: "reset and activation pages: notice when the new password is refused as too weak",
179
+ },
180
+ "front-door.reset-code.primary": { text: "Reset password", context: "reset page, code step: main button" },
181
+ "front-door.activation.label": { text: "Password", context: "activation page: password field label" },
182
+ "front-door.activation-first-name.label": { text: "First name", context: "activation page: first name field label" },
183
+ "front-door.activation-last-name.label": { text: "Last name", context: "activation page: last name field label" },
184
+ "front-door.name-required.notice": { text: "Enter your first and last name.", context: "activation page: notice when a name field is empty" },
185
+ "front-door.sign-in-staff-only.description": { text: "Sign-in is for staff with access to {surface}.", context: "staff-only sign-in page: line under the heading" },
186
+ "front-door.sign-in-invite-only.description": { text: "Sign-in is by invitation to {surface}.", context: "invite-only sign-in page: line under the heading" },
187
+ "front-door.not-authorized-staff-only.title": { text: "Staff access required", context: "staff-only not-authorized page: heading" },
188
+ "front-door.not-authorized-staff-only.description": { text: "This account doesn’t have staff access to {surface}.", context: "staff-only not-authorized page: line under the heading" },
189
+ "front-door.not-authorized-staff-only.primary": { text: "Switch account", context: "staff-only not-authorized page: single primary action" },
190
+ "front-door.not-authorized-invite-only.title": { text: "Invitation required", context: "invite-only not-authorized page: heading" },
191
+ "front-door.not-authorized-invite-only.description": { text: "This account doesn’t have an invitation to {surface}.", context: "invite-only not-authorized page: line under the heading" },
192
+ "front-door.not-authorized-invite-only.primary": { text: "Switch account", context: "invite-only not-authorized page: single primary action" },
193
+ "front-door.identifier-not-found-neutral.notice": { text: "Sign-in couldn’t continue. Check your details and try again.", context: "restricted sign-in page: neutral failure notice without confirming account existence" },
194
+ };
195
+
196
+ for (const entry of Object.values(DEFAULTS)) Object.freeze(entry);
197
+ Object.freeze(DEFAULTS);
198
+
199
+ function placeholdersOf(text: string): string[] {
200
+ return [...new Set([...text.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]!))];
201
+ }
202
+
203
+ function entryFor(id: FrontDoorKey): CopyRegistryEntry {
204
+ const { text, context } = DEFAULTS[id];
205
+ const placeholders = placeholdersOf(text);
206
+ return {
207
+ id,
208
+ text,
209
+ context,
210
+ status: "approved",
211
+ ...(placeholders.length > 0 ? { placeholders } : {}),
212
+ };
213
+ }
214
+
215
+ /** Returns independent mutable defaults for the legacy registry resolver. */
216
+ export function createDefaultCatalog(): CopyRegistry {
217
+ return {
218
+ id: "front-door", locale: "en", revision: "1",
219
+ source: { kind: "imported", reference: "@clossys/writer/front-door.en.json" },
220
+ entries: CANONICAL_IDS.map(entryFor)
221
+ };
222
+ }
223
+
224
+ /** Copies the own, enumerable string-keyed values of `nouns`; anything unreadable reads as no nouns at all. */
225
+ function readNouns(nouns: unknown): Map<string, unknown> {
226
+ const read = new Map<string, unknown>();
227
+ if (typeof nouns !== "object" || nouns === null || Array.isArray(nouns)) return read;
228
+ try {
229
+ for (const name of Object.keys(nouns)) read.set(name, (nouns as Record<string, unknown>)[name]);
230
+ } catch {
231
+ return new Map();
232
+ }
233
+ return read;
234
+ }
235
+
236
+ /** Names a lookup key for a message; a key that cannot be stringified (a null-prototype object, a throwing `toString`) reads as its type. */
237
+ function describeKey(key: unknown): string {
238
+ try {
239
+ return String(key);
240
+ } catch {
241
+ return `<${typeof key}>`;
242
+ }
243
+ }
244
+
245
+ /** Reads and validates nouns without importing registry approval machinery. */
246
+ export function prepareFrontDoorCopy(key: FrontDoorKey, nouns: FrontDoorNouns, catalog: CopyRegistry,
247
+ ids: readonly string[], nounNames: readonly string[]): { complete: false; issues: FrontDoorCopyIssue[] } | { complete: true; entry: CopyRegistryEntry; values: Record<string, string> } {
248
+ if (typeof key !== "string" || !ids.includes(key)) {
249
+ return {
250
+ complete: false as const,
251
+ issues: [{ reason: "unknown-copy-id", id: describeKey(key), message: `Front-door copy id "${describeKey(key)}" is not a reserved id.` }],
252
+ };
253
+ }
254
+
255
+ const entry = catalog.entries.find((candidate) => candidate.id === key);
256
+ if (!entry) {
257
+ return {
258
+ complete: false as const,
259
+ issues: [{ reason: "unknown-copy-id", id: key, message: `Front-door copy "${key}" is not present in the catalog.` }],
260
+ };
261
+ }
262
+
263
+ const supplied = readNouns(nouns);
264
+ const issues: FrontDoorCopyIssue[] = [];
265
+ for (const name of supplied.keys()) {
266
+ if (!nounNames.includes(name)) {
267
+ issues.push({ reason: "unknown-noun", noun: name, message: `"${name}" is not a front-door noun.` });
268
+ }
269
+ }
270
+
271
+ const values: Record<string, string> = {};
272
+ for (const name of entry.placeholders ?? []) {
273
+ const value = supplied.get(name);
274
+ if (typeof value !== "string" || value.trim().length === 0) {
275
+ issues.push({ reason: "missing-noun", id: key, noun: name, message: `Front-door copy "${key}" needs the noun "${name}", which is absent or blank.` });
276
+ } else {
277
+ values[name] = value;
278
+ }
279
+ }
280
+ if (issues.length > 0) return { complete: false as const, issues };
281
+
282
+ return { complete: true as const, entry, values };
283
+ }