@rexezuge/shared 0.0.0-stage → 1.0.1
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.
- package/LICENSE +21 -0
- package/dist/Base64.d.ts +52 -0
- package/dist/Base64.d.ts.map +1 -0
- package/dist/Base64.js +81 -0
- package/dist/Base64.js.map +1 -0
- package/dist/Clock.d.ts +56 -0
- package/dist/Clock.d.ts.map +1 -0
- package/dist/Clock.js +59 -0
- package/dist/Clock.js.map +1 -0
- package/dist/CryptoUtil.d.ts +79 -0
- package/dist/CryptoUtil.d.ts.map +1 -0
- package/dist/CryptoUtil.js +97 -0
- package/dist/CryptoUtil.js.map +1 -0
- package/dist/CursorCodec.d.ts +61 -0
- package/dist/CursorCodec.d.ts.map +1 -0
- package/dist/CursorCodec.js +60 -0
- package/dist/CursorCodec.js.map +1 -0
- package/dist/EmailUtil.d.ts +29 -0
- package/dist/EmailUtil.d.ts.map +1 -0
- package/dist/EmailUtil.js +31 -0
- package/dist/EmailUtil.js.map +1 -0
- package/dist/ErrorSanitizationUtil.d.ts +69 -0
- package/dist/ErrorSanitizationUtil.d.ts.map +1 -0
- package/dist/ErrorSanitizationUtil.js +98 -0
- package/dist/ErrorSanitizationUtil.js.map +1 -0
- package/dist/IdGenerator.d.ts +27 -0
- package/dist/IdGenerator.d.ts.map +1 -0
- package/dist/IdGenerator.js +28 -0
- package/dist/IdGenerator.js.map +1 -0
- package/dist/Identity.d.ts +36 -0
- package/dist/Identity.d.ts.map +1 -0
- package/dist/Identity.js +39 -0
- package/dist/Identity.js.map +1 -0
- package/dist/LanguageTag.d.ts +25 -0
- package/dist/LanguageTag.d.ts.map +1 -0
- package/dist/LanguageTag.js +29 -0
- package/dist/LanguageTag.js.map +1 -0
- package/dist/LocaleUtil.d.ts +35 -0
- package/dist/LocaleUtil.d.ts.map +1 -0
- package/dist/LocaleUtil.js +97 -0
- package/dist/LocaleUtil.js.map +1 -0
- package/dist/PasswordFingerprint.d.ts +30 -0
- package/dist/PasswordFingerprint.d.ts.map +1 -0
- package/dist/PasswordFingerprint.js +33 -0
- package/dist/PasswordFingerprint.js.map +1 -0
- package/dist/RemoteUrlPolicy.d.ts +121 -0
- package/dist/RemoteUrlPolicy.d.ts.map +1 -0
- package/dist/RemoteUrlPolicy.js +339 -0
- package/dist/RemoteUrlPolicy.js.map +1 -0
- package/dist/Result.d.ts +32 -0
- package/dist/Result.d.ts.map +1 -0
- package/dist/Result.js +19 -0
- package/dist/Result.js.map +1 -0
- package/dist/SubrequestMeter.d.ts +204 -0
- package/dist/SubrequestMeter.d.ts.map +1 -0
- package/dist/SubrequestMeter.js +169 -0
- package/dist/SubrequestMeter.js.map +1 -0
- package/dist/TimeZoneUtil.d.ts +47 -0
- package/dist/TimeZoneUtil.d.ts.map +1 -0
- package/dist/TimeZoneUtil.js +101 -0
- package/dist/TimeZoneUtil.js.map +1 -0
- package/dist/TimestampUtil.d.ts +53 -0
- package/dist/TimestampUtil.d.ts.map +1 -0
- package/dist/TimestampUtil.js +69 -0
- package/dist/TimestampUtil.js.map +1 -0
- package/dist/TokenHashUtil.d.ts +34 -0
- package/dist/TokenHashUtil.d.ts.map +1 -0
- package/dist/TokenHashUtil.js +37 -0
- package/dist/TokenHashUtil.js.map +1 -0
- package/dist/UUIDUtil.d.ts +70 -0
- package/dist/UUIDUtil.d.ts.map +1 -0
- package/dist/UUIDUtil.js +79 -0
- package/dist/UUIDUtil.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -3
- package/README.md +0 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isValidEmailFormat, MAX_EMAIL_LENGTH } from './Identity';
|
|
2
|
+
/**
|
|
3
|
+
* Email address value helpers.
|
|
4
|
+
*
|
|
5
|
+
* ## Provenance
|
|
6
|
+
*
|
|
7
|
+
* Converged from Durable-DAV's `EmailUtil.ts` (a shape check) and Mail-Meow's
|
|
8
|
+
* `EmailUtil.ts` (address normalization). Durable-DAV's predicate `isValidEmailFormat`
|
|
9
|
+
* canonically lives in `Identity.ts` — the wider group's home, and the stricter
|
|
10
|
+
* of the two regexes found (see the merge note there) — and is re-exported here
|
|
11
|
+
* so `EmailUtil` is the one-stop surface for validation + normalization.
|
|
12
|
+
*
|
|
13
|
+
* `normalizeEmail` comes from Mail-Meow, whose header records why a single
|
|
14
|
+
* normalization rule is a correctness concern, not a cosmetic one: "This function
|
|
15
|
+
* used to exist four times, and the copies disagreed." One copy trimmed and
|
|
16
|
+
* lowercased, the others lowercased only, so `" Alice@Example.com "` and
|
|
17
|
+
* `"alice@example.com"` were the same account to one layer and different accounts
|
|
18
|
+
* to another — a different account key on each side of a boundary, and a stored
|
|
19
|
+
* registry row that its own service could not resolve.
|
|
20
|
+
*
|
|
21
|
+
* ## Design decision
|
|
22
|
+
*
|
|
23
|
+
* The kit keeps `normalizeEmail` and re-exports the validator; it does not
|
|
24
|
+
* re-introduce Edge-Git's `EmailAddress` value class, which is sugar over the
|
|
25
|
+
* same trim+lowercase and would be a second spelling of the same rule.
|
|
26
|
+
*/
|
|
27
|
+
function normalizeEmail(email) {
|
|
28
|
+
return email.trim().toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
export { normalizeEmail, isValidEmailFormat, MAX_EMAIL_LENGTH };
|
|
31
|
+
//# sourceMappingURL=EmailUtil.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmailUtil.js","sourceRoot":"","sources":["../src/EmailUtil.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACpC,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret-safe error/log sanitization (Layer 0).
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from `ErrorSanitizationUtil.ts` in Durable-DAV, Durable-DAV-Router,
|
|
7
|
+
* Edge-Git, Edge-Sonic, Mail-Meow and Mail-Otter. All six redact the same secret
|
|
8
|
+
* shapes (Bearer/Basic headers, token and secret key=value pairs, query-string
|
|
9
|
+
* tokens, JWTs, Google OAuth tokens) and differ only in which extras they add:
|
|
10
|
+
*
|
|
11
|
+
* - Durable-DAV, Edge-Git, Durable-DAV-Router and Edge-Sonic also redact a
|
|
12
|
+
* repo-specific personal-access-token scheme (`<repo>-pat:<token>`), sanitize
|
|
13
|
+
* the **stack** as well as the message, and expose `stackForLog`.
|
|
14
|
+
* - Mail-Meow and Mail-Otter omit all three and return `name: <redacted message>`
|
|
15
|
+
* without a stack.
|
|
16
|
+
*
|
|
17
|
+
* ## Design decisions
|
|
18
|
+
*
|
|
19
|
+
* - **Stack sanitization is kept.** A stack trace repeats the error message and
|
|
20
|
+
* can name the very secret the message carries, so redacting the message but
|
|
21
|
+
* logging the raw stack — which Mail-Meow/Mail-Otter effectively do — is a
|
|
22
|
+
* one-line leak. This is the stricter of the two shapes, so it is the one kept.
|
|
23
|
+
* `stackForLog` stays for the structured-log payload path.
|
|
24
|
+
* - **The PAT scheme is generalized, not dropped and not repo-hardcoded.** Four
|
|
25
|
+
* repos each added their own `durable-dav-pat:` / `edge-git-pat:` /
|
|
26
|
+
* `durable-dav-router-pat:` / `edge-sonic-pat:` literal. Hard-coding those
|
|
27
|
+
* prefixes here would leak five repo identities into a shared package; dropping
|
|
28
|
+
* the class would undo a safeguard four of six repos independently judged
|
|
29
|
+
* necessary. The converged form is one generic `<word>-pat:<token>` pattern
|
|
30
|
+
* that covers every repo's scheme without naming any.
|
|
31
|
+
* - **Two correctness lessons from Mail-Otter's header are preserved in code and
|
|
32
|
+
* comment.** `\bbearer`/`\bbasic` keep the leading word boundary — without it
|
|
33
|
+
* the pattern parsed as "word boundary, then the literal `earer`", matched
|
|
34
|
+
* nothing, and Authorization headers leaked verbatim while the suite stayed
|
|
35
|
+
* green only because a Google token in the sample was caught elsewhere. And the
|
|
36
|
+
* `SECRET_KV` set takes Mail-Meow's superset (`client_id`, bare `code`) over the
|
|
37
|
+
* narrower alternatives.
|
|
38
|
+
*/
|
|
39
|
+
declare class ErrorSanitizationUtil {
|
|
40
|
+
/**
|
|
41
|
+
* Redact every recognizable secret shape from a single string.
|
|
42
|
+
*
|
|
43
|
+
* Replacements are inline string literals (not variables or templates) so a
|
|
44
|
+
* `unicorn/no-unsafe-string-replacement`-style rule is satisfied and so CodeQL
|
|
45
|
+
* models each `.replaceAll(regex, literal)` as a `js/clear-text-logging` masking
|
|
46
|
+
* barrier. Order matters only in that the PAT scheme runs first; the rest are
|
|
47
|
+
* disjoint.
|
|
48
|
+
*/
|
|
49
|
+
static sanitizeMessage(message: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* A redacted `name: message` (and stack) for a plain log line.
|
|
52
|
+
*
|
|
53
|
+
* Both the message and the stack are sanitized — see the header for why the
|
|
54
|
+
* stack must be.
|
|
55
|
+
*/
|
|
56
|
+
static sanitizeErrorForLogging(error: unknown): string;
|
|
57
|
+
/**
|
|
58
|
+
* A redacted error, for a structured log payload.
|
|
59
|
+
*
|
|
60
|
+
* `error instanceof Error ? (error.stack ?? error.message) : error` was written
|
|
61
|
+
* out at many call sites, and every one of them bypassed the redaction above —
|
|
62
|
+
* so an error whose message carried a credential would be logged in the clear. A
|
|
63
|
+
* logging helper that only redacts when the caller remembers to call it is not a
|
|
64
|
+
* redaction boundary; this is the single point every payload goes through.
|
|
65
|
+
*/
|
|
66
|
+
static stackForLog(error: unknown): unknown;
|
|
67
|
+
}
|
|
68
|
+
export { ErrorSanitizationUtil };
|
|
69
|
+
//# sourceMappingURL=ErrorSanitizationUtil.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorSanitizationUtil.d.ts","sourceRoot":"","sources":["../src/ErrorSanitizationUtil.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAaH,cAAM,qBAAqB;IACzB;;;;;;;;OAQG;WACW,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAYtD;;;;;OAKG;WACW,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;IAS7D;;;;;;;;OAQG;WACW,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;CAGnD;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret-safe error/log sanitization (Layer 0).
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from `ErrorSanitizationUtil.ts` in Durable-DAV, Durable-DAV-Router,
|
|
7
|
+
* Edge-Git, Edge-Sonic, Mail-Meow and Mail-Otter. All six redact the same secret
|
|
8
|
+
* shapes (Bearer/Basic headers, token and secret key=value pairs, query-string
|
|
9
|
+
* tokens, JWTs, Google OAuth tokens) and differ only in which extras they add:
|
|
10
|
+
*
|
|
11
|
+
* - Durable-DAV, Edge-Git, Durable-DAV-Router and Edge-Sonic also redact a
|
|
12
|
+
* repo-specific personal-access-token scheme (`<repo>-pat:<token>`), sanitize
|
|
13
|
+
* the **stack** as well as the message, and expose `stackForLog`.
|
|
14
|
+
* - Mail-Meow and Mail-Otter omit all three and return `name: <redacted message>`
|
|
15
|
+
* without a stack.
|
|
16
|
+
*
|
|
17
|
+
* ## Design decisions
|
|
18
|
+
*
|
|
19
|
+
* - **Stack sanitization is kept.** A stack trace repeats the error message and
|
|
20
|
+
* can name the very secret the message carries, so redacting the message but
|
|
21
|
+
* logging the raw stack — which Mail-Meow/Mail-Otter effectively do — is a
|
|
22
|
+
* one-line leak. This is the stricter of the two shapes, so it is the one kept.
|
|
23
|
+
* `stackForLog` stays for the structured-log payload path.
|
|
24
|
+
* - **The PAT scheme is generalized, not dropped and not repo-hardcoded.** Four
|
|
25
|
+
* repos each added their own `durable-dav-pat:` / `edge-git-pat:` /
|
|
26
|
+
* `durable-dav-router-pat:` / `edge-sonic-pat:` literal. Hard-coding those
|
|
27
|
+
* prefixes here would leak five repo identities into a shared package; dropping
|
|
28
|
+
* the class would undo a safeguard four of six repos independently judged
|
|
29
|
+
* necessary. The converged form is one generic `<word>-pat:<token>` pattern
|
|
30
|
+
* that covers every repo's scheme without naming any.
|
|
31
|
+
* - **Two correctness lessons from Mail-Otter's header are preserved in code and
|
|
32
|
+
* comment.** `\bbearer`/`\bbasic` keep the leading word boundary — without it
|
|
33
|
+
* the pattern parsed as "word boundary, then the literal `earer`", matched
|
|
34
|
+
* nothing, and Authorization headers leaked verbatim while the suite stayed
|
|
35
|
+
* green only because a Google token in the sample was caught elsewhere. And the
|
|
36
|
+
* `SECRET_KV` set takes Mail-Meow's superset (`client_id`, bare `code`) over the
|
|
37
|
+
* narrower alternatives.
|
|
38
|
+
*/
|
|
39
|
+
const BEARER_PATTERN = /\bbearer\s+\S+/gi;
|
|
40
|
+
const BASIC_PATTERN = /\bbasic\s+\S+/gi;
|
|
41
|
+
// Any repo's personal-access-token scheme: `<something>-pat:<token>`. Generic on
|
|
42
|
+
// purpose — see the header — so no specific repo's prefix is baked in.
|
|
43
|
+
const PAT_PATTERN = /\b[\w-]+-pat:[\w-]+/gi;
|
|
44
|
+
const TOKEN_KV_PATTERN = /((?:access|refresh|id)[_-]?token\s*[:=]\s*).\S*/gi;
|
|
45
|
+
const SECRET_KV_PATTERN = /((?:client[_-]?secret|client[_-]?id|auth[_-]?code|code[_-]?verifier|\bcode)\s*[:=]\s*).\S*/gi;
|
|
46
|
+
const TOKEN_QUERY_PATTERN = /([?&](?:access_token|refresh_token|code|client_secret)=).[^&\s;}]*/gi;
|
|
47
|
+
const JWT_PATTERN = /eyJ[\w-].+[\w-].+[\w./+=~-]/g;
|
|
48
|
+
const GOOGLE_TOKEN_PATTERN = /ya29..[\w.-]+/g;
|
|
49
|
+
class ErrorSanitizationUtil {
|
|
50
|
+
/**
|
|
51
|
+
* Redact every recognizable secret shape from a single string.
|
|
52
|
+
*
|
|
53
|
+
* Replacements are inline string literals (not variables or templates) so a
|
|
54
|
+
* `unicorn/no-unsafe-string-replacement`-style rule is satisfied and so CodeQL
|
|
55
|
+
* models each `.replaceAll(regex, literal)` as a `js/clear-text-logging` masking
|
|
56
|
+
* barrier. Order matters only in that the PAT scheme runs first; the rest are
|
|
57
|
+
* disjoint.
|
|
58
|
+
*/
|
|
59
|
+
static sanitizeMessage(message) {
|
|
60
|
+
return message
|
|
61
|
+
.replaceAll(PAT_PATTERN, '[REDACTED-PAT]')
|
|
62
|
+
.replaceAll(BEARER_PATTERN, 'Bearer [REDACTED]')
|
|
63
|
+
.replaceAll(BASIC_PATTERN, 'Basic [REDACTED]')
|
|
64
|
+
.replaceAll(TOKEN_KV_PATTERN, '$1[REDACTED]')
|
|
65
|
+
.replaceAll(SECRET_KV_PATTERN, '$1[REDACTED]')
|
|
66
|
+
.replaceAll(TOKEN_QUERY_PATTERN, '$1[REDACTED]')
|
|
67
|
+
.replaceAll(JWT_PATTERN, '[REDACTED-JWT]')
|
|
68
|
+
.replaceAll(GOOGLE_TOKEN_PATTERN, '[REDACTED]');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A redacted `name: message` (and stack) for a plain log line.
|
|
72
|
+
*
|
|
73
|
+
* Both the message and the stack are sanitized — see the header for why the
|
|
74
|
+
* stack must be.
|
|
75
|
+
*/
|
|
76
|
+
static sanitizeErrorForLogging(error) {
|
|
77
|
+
if (error instanceof Error) {
|
|
78
|
+
const redacted = this.sanitizeMessage(error.message);
|
|
79
|
+
const stack = error.stack ? `\n${this.sanitizeMessage(error.stack)}` : '';
|
|
80
|
+
return `${error.name}: ${redacted}${stack}`;
|
|
81
|
+
}
|
|
82
|
+
return this.sanitizeMessage(String(error));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A redacted error, for a structured log payload.
|
|
86
|
+
*
|
|
87
|
+
* `error instanceof Error ? (error.stack ?? error.message) : error` was written
|
|
88
|
+
* out at many call sites, and every one of them bypassed the redaction above —
|
|
89
|
+
* so an error whose message carried a credential would be logged in the clear. A
|
|
90
|
+
* logging helper that only redacts when the caller remembers to call it is not a
|
|
91
|
+
* redaction boundary; this is the single point every payload goes through.
|
|
92
|
+
*/
|
|
93
|
+
static stackForLog(error) {
|
|
94
|
+
return this.sanitizeErrorForLogging(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export { ErrorSanitizationUtil };
|
|
98
|
+
//# sourceMappingURL=ErrorSanitizationUtil.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorSanitizationUtil.js","sourceRoot":"","sources":["../src/ErrorSanitizationUtil.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,MAAM,cAAc,GAAG,kBAAkB,CAAC;AAC1C,MAAM,aAAa,GAAG,iBAAiB,CAAC;AACxC,iFAAiF;AACjF,uEAAuE;AACvE,MAAM,WAAW,GAAG,uBAAuB,CAAC;AAC5C,MAAM,gBAAgB,GAAG,mDAAmD,CAAC;AAC7E,MAAM,iBAAiB,GAAG,8FAA8F,CAAC;AACzH,MAAM,mBAAmB,GAAG,sEAAsE,CAAC;AACnG,MAAM,WAAW,GAAG,8BAA8B,CAAC;AACnD,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AAE9C,MAAM,qBAAqB;IACzB;;;;;;;;OAQG;IACI,MAAM,CAAC,eAAe,CAAC,OAAe;QAC3C,OAAO,OAAO;aACX,UAAU,CAAC,WAAW,EAAE,gBAAgB,CAAC;aACzC,UAAU,CAAC,cAAc,EAAE,mBAAmB,CAAC;aAC/C,UAAU,CAAC,aAAa,EAAE,kBAAkB,CAAC;aAC7C,UAAU,CAAC,gBAAgB,EAAE,cAAc,CAAC;aAC5C,UAAU,CAAC,iBAAiB,EAAE,cAAc,CAAC;aAC7C,UAAU,CAAC,mBAAmB,EAAE,cAAc,CAAC;aAC/C,UAAU,CAAC,WAAW,EAAE,gBAAgB,CAAC;aACzC,UAAU,CAAC,oBAAoB,EAAE,YAAY,CAAC,CAAC;IACpD,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,uBAAuB,CAAC,KAAc;QAClD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1E,OAAO,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ,GAAG,KAAK,EAAE,CAAC;QAC9C,CAAC;QACD,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED;;;;;;;;OAQG;IACI,MAAM,CAAC,WAAW,CAAC,KAAc;QACtC,OAAO,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;CACF;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifiers behind an interface, so ids can be pinned in a test.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Taken from ChordDHT-Tracker's `IdGenerator.ts` — the only repo that factored
|
|
7
|
+
* id generation behind a seam. `CryptoIdGenerator` is the production path (one
|
|
8
|
+
* `crypto.randomUUID`), `FixedIdGenerator` is the test double that replays a
|
|
9
|
+
* queued set and then synthesizes deterministic stand-ins so a test can assert
|
|
10
|
+
* on the exact ids a code path produced.
|
|
11
|
+
*/
|
|
12
|
+
interface IdGenerator {
|
|
13
|
+
randomUUID(): string;
|
|
14
|
+
}
|
|
15
|
+
declare class CryptoIdGenerator implements IdGenerator {
|
|
16
|
+
randomUUID(): string;
|
|
17
|
+
}
|
|
18
|
+
declare class FixedIdGenerator implements IdGenerator {
|
|
19
|
+
private readonly queue;
|
|
20
|
+
private counter;
|
|
21
|
+
constructor(ids?: string | string[]);
|
|
22
|
+
queueId(id: string): void;
|
|
23
|
+
randomUUID(): string;
|
|
24
|
+
}
|
|
25
|
+
export { CryptoIdGenerator, FixedIdGenerator };
|
|
26
|
+
export type { IdGenerator };
|
|
27
|
+
//# sourceMappingURL=IdGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IdGenerator.d.ts","sourceRoot":"","sources":["../src/IdGenerator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,UAAU,WAAW;IACnB,UAAU,IAAI,MAAM,CAAC;CACtB;AAED,cAAM,iBAAkB,YAAW,WAAW;IACrC,UAAU,IAAI,MAAM;CAG5B;AAED,cAAM,gBAAiB,YAAW,WAAW;IAC3C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,OAAO,CAAK;gBAER,GAAG,GAAE,MAAM,GAAG,MAAM,EAAO;IAOhC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIzB,UAAU,IAAI,MAAM;CAM5B;AAED,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;AAC/C,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class CryptoIdGenerator {
|
|
2
|
+
randomUUID() {
|
|
3
|
+
return crypto.randomUUID();
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
class FixedIdGenerator {
|
|
7
|
+
queue = [];
|
|
8
|
+
counter = 0;
|
|
9
|
+
constructor(ids = []) {
|
|
10
|
+
const initial = Array.isArray(ids) ? ids : [ids];
|
|
11
|
+
for (const id of initial) {
|
|
12
|
+
if (id)
|
|
13
|
+
this.queue.push(id);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
queueId(id) {
|
|
17
|
+
this.queue.push(id);
|
|
18
|
+
}
|
|
19
|
+
randomUUID() {
|
|
20
|
+
const next = this.queue.shift();
|
|
21
|
+
if (next !== undefined)
|
|
22
|
+
return next;
|
|
23
|
+
this.counter += 1;
|
|
24
|
+
return `fixed-uuid-${this.counter.toString()}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export { CryptoIdGenerator, FixedIdGenerator };
|
|
28
|
+
//# sourceMappingURL=IdGenerator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IdGenerator.js","sourceRoot":"","sources":["../src/IdGenerator.ts"],"names":[],"mappings":"AAeA,MAAM,iBAAiB;IACd,UAAU;QACf,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;CACF;AAED,MAAM,gBAAgB;IACH,KAAK,GAAa,EAAE,CAAC;IAC9B,OAAO,GAAG,CAAC,CAAC;IAEpB,YAAY,MAAyB,EAAE;QACrC,MAAM,OAAO,GAAa,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC3D,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;YACzB,IAAI,EAAE;gBAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAEM,OAAO,CAAC,EAAU;QACvB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAEM,UAAU;QACf,MAAM,IAAI,GAAuB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACpD,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;QAClB,OAAO,cAAc,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;IACjD,CAAC;CACF;AAED,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-string well-formedness (Layer 0).
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from Durable-DAV-Router's, Edge-Git's and Edge-Sonic's
|
|
7
|
+
* `Identity.ts`. Router and Edge-Sonic are byte-identical and hold exactly one
|
|
8
|
+
* helper — `isValidEmailFormat` — described as "a cheap sanity check on an
|
|
9
|
+
* identity the service did not choose". Edge-Git's is larger: the same predicate
|
|
10
|
+
* plus an `EmailAddress` value object and a `RepoFullName`/`repoDoKey` pair. The
|
|
11
|
+
* value object duplicates `normalizeEmail` (see `EmailUtil.ts`); the
|
|
12
|
+
* `owner/name` repo identifier, `.git`-stripping and Durable-Object key helpers
|
|
13
|
+
* are **git business logic and are left local** — they are the same kind of
|
|
14
|
+
* repo-specific concern as `GitOids`/`BranchValidation`.
|
|
15
|
+
*
|
|
16
|
+
* ## Design decision
|
|
17
|
+
*
|
|
18
|
+
* This file is the definitional home of `isValidEmailFormat` because that is what
|
|
19
|
+
* the wider group (Router, Edge-Sonic, Edge-Git) file under "Identity". Two
|
|
20
|
+
* regexes circulated for the same predicate: Durable-DAV's
|
|
21
|
+
* `/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/` and the Router/Edge-Git/Sonic
|
|
22
|
+
* `/^[^@\s]+@[^\s@][^\s.@]*\.[^\s@]+$/`. Durable-DAV's is the **stricter** one —
|
|
23
|
+
* it rejects a leading dot in the domain (`a@.b.c`) and consecutive dots, which
|
|
24
|
+
* the other admits — so per the kit rule the stricter form is kept. `EmailUtil`
|
|
25
|
+
* re-exports the predicate so the whole email surface is one import.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately permissive about the local part (RFC 5322 allows far more than
|
|
28
|
+
* this) and strict about the obvious mistakes: no whitespace, exactly one `@`,
|
|
29
|
+
* a dot-separated domain, and a bounded total length. It is a shape check, not a
|
|
30
|
+
* deliverability check — the authoritative validation is the identity provider.
|
|
31
|
+
*/
|
|
32
|
+
declare const EMAIL_FORMAT_RE: RegExp;
|
|
33
|
+
declare const MAX_EMAIL_LENGTH = 254;
|
|
34
|
+
declare function isValidEmailFormat(raw: string): boolean;
|
|
35
|
+
export { isValidEmailFormat, MAX_EMAIL_LENGTH, EMAIL_FORMAT_RE };
|
|
36
|
+
//# sourceMappingURL=Identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Identity.d.ts","sourceRoot":"","sources":["../src/Identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,QAAA,MAAM,eAAe,QAAsC,CAAC;AAE5D,QAAA,MAAM,gBAAgB,MAAM,CAAC;AAE7B,iBAAS,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGhD;AAED,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/Identity.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-string well-formedness (Layer 0).
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from Durable-DAV-Router's, Edge-Git's and Edge-Sonic's
|
|
7
|
+
* `Identity.ts`. Router and Edge-Sonic are byte-identical and hold exactly one
|
|
8
|
+
* helper — `isValidEmailFormat` — described as "a cheap sanity check on an
|
|
9
|
+
* identity the service did not choose". Edge-Git's is larger: the same predicate
|
|
10
|
+
* plus an `EmailAddress` value object and a `RepoFullName`/`repoDoKey` pair. The
|
|
11
|
+
* value object duplicates `normalizeEmail` (see `EmailUtil.ts`); the
|
|
12
|
+
* `owner/name` repo identifier, `.git`-stripping and Durable-Object key helpers
|
|
13
|
+
* are **git business logic and are left local** — they are the same kind of
|
|
14
|
+
* repo-specific concern as `GitOids`/`BranchValidation`.
|
|
15
|
+
*
|
|
16
|
+
* ## Design decision
|
|
17
|
+
*
|
|
18
|
+
* This file is the definitional home of `isValidEmailFormat` because that is what
|
|
19
|
+
* the wider group (Router, Edge-Sonic, Edge-Git) file under "Identity". Two
|
|
20
|
+
* regexes circulated for the same predicate: Durable-DAV's
|
|
21
|
+
* `/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/` and the Router/Edge-Git/Sonic
|
|
22
|
+
* `/^[^@\s]+@[^\s@][^\s.@]*\.[^\s@]+$/`. Durable-DAV's is the **stricter** one —
|
|
23
|
+
* it rejects a leading dot in the domain (`a@.b.c`) and consecutive dots, which
|
|
24
|
+
* the other admits — so per the kit rule the stricter form is kept. `EmailUtil`
|
|
25
|
+
* re-exports the predicate so the whole email surface is one import.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately permissive about the local part (RFC 5322 allows far more than
|
|
28
|
+
* this) and strict about the obvious mistakes: no whitespace, exactly one `@`,
|
|
29
|
+
* a dot-separated domain, and a bounded total length. It is a shape check, not a
|
|
30
|
+
* deliverability check — the authoritative validation is the identity provider.
|
|
31
|
+
*/
|
|
32
|
+
const EMAIL_FORMAT_RE = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
|
|
33
|
+
const MAX_EMAIL_LENGTH = 254;
|
|
34
|
+
function isValidEmailFormat(raw) {
|
|
35
|
+
const trimmed = raw.trim();
|
|
36
|
+
return trimmed !== '' && trimmed.length <= MAX_EMAIL_LENGTH && EMAIL_FORMAT_RE.test(trimmed);
|
|
37
|
+
}
|
|
38
|
+
export { isValidEmailFormat, MAX_EMAIL_LENGTH, EMAIL_FORMAT_RE };
|
|
39
|
+
//# sourceMappingURL=Identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Identity.js","sourceRoot":"","sources":["../src/Identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,eAAe,GAAG,mCAAmC,CAAC;AAE5D,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,SAAS,kBAAkB,CAAC,GAAW;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,OAAO,OAAO,KAAK,EAAE,IAAI,OAAO,CAAC,MAAM,IAAI,gBAAgB,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC/F,CAAC;AAED,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single canonical language-tag implementation for backend + web.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from Durable-DAV-Router's `LanguageTag.ts` and Edge-Git's
|
|
7
|
+
* `LanguageTag.ts`. The two are byte-identical — both were written to replace
|
|
8
|
+
* the older `canonicalizeBackendLocaleTag` (shared) / `canonicalizeTag` (web
|
|
9
|
+
* `i18n.ts`) split, which had drifted. Edge-Sonic has no such file (its web layer
|
|
10
|
+
* uses the shared one directly), so it contributes nothing here beyond confirming
|
|
11
|
+
* the shape.
|
|
12
|
+
*
|
|
13
|
+
* Import from here in both layers. This is generic BCP-47 casing, distinct from
|
|
14
|
+
* `LocaleUtil`'s `canonicalizeLocaleTag`, which additionally titlecases a
|
|
15
|
+
* four-letter script subtag for locale negotiation against a fixed table.
|
|
16
|
+
*/
|
|
17
|
+
declare function canonicalizeLanguageTag(tag: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* A BCP-47 language tag, canonicalized: `zh_TW`, `zh-tw`, and `zh-TW` all
|
|
20
|
+
* canonicalize to `zh-TW`.
|
|
21
|
+
*/
|
|
22
|
+
type LanguageTag = string;
|
|
23
|
+
export { canonicalizeLanguageTag };
|
|
24
|
+
export type { LanguageTag };
|
|
25
|
+
//# sourceMappingURL=LanguageTag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LanguageTag.d.ts","sourceRoot":"","sources":["../src/LanguageTag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,iBAAS,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQpD;AAED;;;GAGG;AACH,KAAK,WAAW,GAAG,MAAM,CAAC;AAE1B,OAAO,EAAE,uBAAuB,EAAE,CAAC;AACnC,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single canonical language-tag implementation for backend + web.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from Durable-DAV-Router's `LanguageTag.ts` and Edge-Git's
|
|
7
|
+
* `LanguageTag.ts`. The two are byte-identical — both were written to replace
|
|
8
|
+
* the older `canonicalizeBackendLocaleTag` (shared) / `canonicalizeTag` (web
|
|
9
|
+
* `i18n.ts`) split, which had drifted. Edge-Sonic has no such file (its web layer
|
|
10
|
+
* uses the shared one directly), so it contributes nothing here beyond confirming
|
|
11
|
+
* the shape.
|
|
12
|
+
*
|
|
13
|
+
* Import from here in both layers. This is generic BCP-47 casing, distinct from
|
|
14
|
+
* `LocaleUtil`'s `canonicalizeLocaleTag`, which additionally titlecases a
|
|
15
|
+
* four-letter script subtag for locale negotiation against a fixed table.
|
|
16
|
+
*/
|
|
17
|
+
function canonicalizeLanguageTag(tag) {
|
|
18
|
+
const normalized = tag.trim().replaceAll('_', '-');
|
|
19
|
+
const parts = normalized.split('-').filter(Boolean);
|
|
20
|
+
if (parts.length === 0)
|
|
21
|
+
return 'en';
|
|
22
|
+
const language = (parts[0] ?? 'en').toLowerCase();
|
|
23
|
+
if (parts.length === 1)
|
|
24
|
+
return language;
|
|
25
|
+
const rest = parts.slice(1).map((part) => (part.length === 2 ? part.toUpperCase() : part.toLowerCase()));
|
|
26
|
+
return [language, ...rest].join('-');
|
|
27
|
+
}
|
|
28
|
+
export { canonicalizeLanguageTag };
|
|
29
|
+
//# sourceMappingURL=LanguageTag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LanguageTag.js","sourceRoot":"","sources":["../src/LanguageTag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,SAAS,uBAAuB,CAAC,GAAW;IAC1C,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IACxC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACzG,OAAO,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC;AAQD,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale negotiation against a fixed support table.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from AWS-AccessBridge's, Mail-Meow's and Mail-Otter's
|
|
7
|
+
* `LocaleUtil.ts`. The three share the same 12-locale table
|
|
8
|
+
* (`en, de, fr, es, it, nl, pt, pl, ja, zh-CN, zh-TW, ko`) and the same
|
|
9
|
+
* `isSupported`/`normalize`; Mail-Meow and Mail-Otter additionally carry
|
|
10
|
+
* `negotiate`, `displayName` and `LOCALE_DISPLAY_NAMES`. The two fuller forms are
|
|
11
|
+
* byte-identical and are kept — AWS is the subset.
|
|
12
|
+
*
|
|
13
|
+
* ## Design decision
|
|
14
|
+
*
|
|
15
|
+
* The kit kept the fuller Mail-Meow/Mail-Otter surface. `normalize` is the load-
|
|
16
|
+
* bearing operation: it canonicalizes a tag, maps regional fallbacks toward the
|
|
17
|
+
* supported table (a bare `zh` becomes `zh-CN`), and — distinct from the generic
|
|
18
|
+
* `canonicalizeLanguageTag` — titlecases a four-letter script subtag so
|
|
19
|
+
* `zh-hans-cn` canonicalizes correctly before negotiation. Non-negotiable input
|
|
20
|
+
* falls back to `DEFAULT_LOCALE` rather than throwing; an Accept-Language header
|
|
21
|
+
* is a hint, not a contract.
|
|
22
|
+
*/
|
|
23
|
+
declare const DEFAULT_LOCALE = "en";
|
|
24
|
+
declare const SUPPORTED_LOCALES: readonly ["en", "de", "fr", "es", "it", "nl", "pt", "pl", "ja", "zh-CN", "zh-TW", "ko"];
|
|
25
|
+
type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
|
26
|
+
declare const LOCALE_DISPLAY_NAMES: Record<SupportedLocale, string>;
|
|
27
|
+
declare class LocaleUtil {
|
|
28
|
+
static isSupported(locale: string | null | undefined): boolean;
|
|
29
|
+
static normalize(locale: string | null | undefined): SupportedLocale;
|
|
30
|
+
static negotiate(preferred: string | null | undefined, fallback?: string | null): SupportedLocale;
|
|
31
|
+
static displayName(locale: string | null | undefined): string;
|
|
32
|
+
}
|
|
33
|
+
export { LocaleUtil, DEFAULT_LOCALE, SUPPORTED_LOCALES, LOCALE_DISPLAY_NAMES };
|
|
34
|
+
export type { SupportedLocale };
|
|
35
|
+
//# sourceMappingURL=LocaleUtil.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocaleUtil.d.ts","sourceRoot":"","sources":["../src/LocaleUtil.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,QAAA,MAAM,cAAc,OAAO,CAAC;AAE5B,QAAA,MAAM,iBAAiB,yFAA0F,CAAC;AAElH,KAAK,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,QAAA,MAAM,oBAAoB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAazD,CAAC;AAiBF,cAAM,UAAU;WACA,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO;WAMvD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe;WAqB7D,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe;WAK1F,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM;CAIrE;AAED,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale negotiation against a fixed support table.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Converged from AWS-AccessBridge's, Mail-Meow's and Mail-Otter's
|
|
7
|
+
* `LocaleUtil.ts`. The three share the same 12-locale table
|
|
8
|
+
* (`en, de, fr, es, it, nl, pt, pl, ja, zh-CN, zh-TW, ko`) and the same
|
|
9
|
+
* `isSupported`/`normalize`; Mail-Meow and Mail-Otter additionally carry
|
|
10
|
+
* `negotiate`, `displayName` and `LOCALE_DISPLAY_NAMES`. The two fuller forms are
|
|
11
|
+
* byte-identical and are kept — AWS is the subset.
|
|
12
|
+
*
|
|
13
|
+
* ## Design decision
|
|
14
|
+
*
|
|
15
|
+
* The kit kept the fuller Mail-Meow/Mail-Otter surface. `normalize` is the load-
|
|
16
|
+
* bearing operation: it canonicalizes a tag, maps regional fallbacks toward the
|
|
17
|
+
* supported table (a bare `zh` becomes `zh-CN`), and — distinct from the generic
|
|
18
|
+
* `canonicalizeLanguageTag` — titlecases a four-letter script subtag so
|
|
19
|
+
* `zh-hans-cn` canonicalizes correctly before negotiation. Non-negotiable input
|
|
20
|
+
* falls back to `DEFAULT_LOCALE` rather than throwing; an Accept-Language header
|
|
21
|
+
* is a hint, not a contract.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_LOCALE = 'en';
|
|
24
|
+
const SUPPORTED_LOCALES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'pl', 'ja', 'zh-CN', 'zh-TW', 'ko'];
|
|
25
|
+
const LOCALE_DISPLAY_NAMES = {
|
|
26
|
+
en: 'English',
|
|
27
|
+
de: 'Deutsch',
|
|
28
|
+
fr: 'Français',
|
|
29
|
+
es: 'Español',
|
|
30
|
+
it: 'Italiano',
|
|
31
|
+
nl: 'Nederlands',
|
|
32
|
+
pt: 'Português',
|
|
33
|
+
pl: 'Polski',
|
|
34
|
+
ja: '日本語',
|
|
35
|
+
'zh-CN': '简体中文',
|
|
36
|
+
'zh-TW': '繁體中文',
|
|
37
|
+
ko: '한국어',
|
|
38
|
+
};
|
|
39
|
+
function canonicalizeLocaleTag(tag) {
|
|
40
|
+
const normalized = tag.trim().replaceAll('_', '-');
|
|
41
|
+
const parts = normalized.split('-').filter(Boolean);
|
|
42
|
+
if (parts.length === 0)
|
|
43
|
+
return '';
|
|
44
|
+
const language = (parts[0] ?? '').toLowerCase();
|
|
45
|
+
if (parts.length === 1)
|
|
46
|
+
return language;
|
|
47
|
+
const rest = parts.slice(1).map((part, index) => {
|
|
48
|
+
if (index === parts.length - 2 && part.length === 4) {
|
|
49
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
return part.length === 2 ? part.toUpperCase() : part.toLowerCase();
|
|
52
|
+
});
|
|
53
|
+
return [language, ...rest].join('-');
|
|
54
|
+
}
|
|
55
|
+
class LocaleUtil {
|
|
56
|
+
static isSupported(locale) {
|
|
57
|
+
if (!locale || typeof locale !== 'string')
|
|
58
|
+
return false;
|
|
59
|
+
const canonical = canonicalizeLocaleTag(locale);
|
|
60
|
+
return SUPPORTED_LOCALES.includes(canonical);
|
|
61
|
+
}
|
|
62
|
+
static normalize(locale) {
|
|
63
|
+
if (!locale || typeof locale !== 'string')
|
|
64
|
+
return DEFAULT_LOCALE;
|
|
65
|
+
const canonical = canonicalizeLocaleTag(locale);
|
|
66
|
+
if (SUPPORTED_LOCALES.includes(canonical)) {
|
|
67
|
+
return canonical;
|
|
68
|
+
}
|
|
69
|
+
const base = canonical.split('-', 1)[0]?.toLowerCase() ?? '';
|
|
70
|
+
if (base === 'zh')
|
|
71
|
+
return 'zh-CN';
|
|
72
|
+
const baseMatch = SUPPORTED_LOCALES.find((s) => s.toLowerCase() === base);
|
|
73
|
+
if (baseMatch)
|
|
74
|
+
return baseMatch;
|
|
75
|
+
try {
|
|
76
|
+
const negotiated = Intl.getCanonicalLocales(canonical);
|
|
77
|
+
const primary = negotiated[0]?.split('-', 1)[0]?.toLowerCase() ?? '';
|
|
78
|
+
const fallback = SUPPORTED_LOCALES.find((s) => s.toLowerCase() === primary);
|
|
79
|
+
if (fallback)
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore invalid tags and fall through to default.
|
|
84
|
+
}
|
|
85
|
+
return DEFAULT_LOCALE;
|
|
86
|
+
}
|
|
87
|
+
static negotiate(preferred, fallback) {
|
|
88
|
+
const primary = this.normalize(preferred);
|
|
89
|
+
return primary === DEFAULT_LOCALE ? this.normalize(fallback) : primary;
|
|
90
|
+
}
|
|
91
|
+
static displayName(locale) {
|
|
92
|
+
const normalized = this.normalize(locale);
|
|
93
|
+
return LOCALE_DISPLAY_NAMES[normalized] ?? normalized;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export { LocaleUtil, DEFAULT_LOCALE, SUPPORTED_LOCALES, LOCALE_DISPLAY_NAMES };
|
|
97
|
+
//# sourceMappingURL=LocaleUtil.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocaleUtil.js","sourceRoot":"","sources":["../src/LocaleUtil.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cheap, deterministic tag for a presented password.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Taken from Durable-DAV's `packages/shared/src/utils/PasswordFingerprint.ts`
|
|
7
|
+
* (the only repo that factored this). It is one unsalted SHA-256 that delegates
|
|
8
|
+
* to `CryptoUtil.sha256Hex`.
|
|
9
|
+
*
|
|
10
|
+
* **Not a credential, and not a secret.** It is one unsalted SHA-256, so it is
|
|
11
|
+
* exactly as crackable as the legacy unsalted digest it is sometimes confused
|
|
12
|
+
* with — a rainbow table recovers it instantly, and two users who chose the same
|
|
13
|
+
* password share a value. It exists to be a memo *key* and nothing else. Any
|
|
14
|
+
* code that treats it as a password hash is wrong.
|
|
15
|
+
*
|
|
16
|
+
* ## Why it exists
|
|
17
|
+
*
|
|
18
|
+
* Password verification is memoized so a repeated request does not pay a PBKDF2
|
|
19
|
+
* derivation again. The memo must distinguish "this exact password was already
|
|
20
|
+
* verified against this stored hash" from "someone presented a different password
|
|
21
|
+
* for this username". Keying on `username` alone would accept **any** password
|
|
22
|
+
* for a recently-used username — a total authentication bypass. The lookup key
|
|
23
|
+
* therefore includes this fingerprint: it makes the key specific to the password
|
|
24
|
+
* actually presented, at the cost of one SHA-256 (microseconds) instead of one
|
|
25
|
+
* PBKDF2 derivation (milliseconds, and on the Workers Free plan more than the
|
|
26
|
+
* entire CPU budget).
|
|
27
|
+
*/
|
|
28
|
+
declare function passwordFingerprint(password: string): Promise<string>;
|
|
29
|
+
export { passwordFingerprint };
|
|
30
|
+
//# sourceMappingURL=PasswordFingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PasswordFingerprint.d.ts","sourceRoot":"","sources":["../src/PasswordFingerprint.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,iBAAe,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEpE;AAED,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CryptoUtil } from './CryptoUtil';
|
|
2
|
+
/**
|
|
3
|
+
* A cheap, deterministic tag for a presented password.
|
|
4
|
+
*
|
|
5
|
+
* ## Provenance
|
|
6
|
+
*
|
|
7
|
+
* Taken from Durable-DAV's `packages/shared/src/utils/PasswordFingerprint.ts`
|
|
8
|
+
* (the only repo that factored this). It is one unsalted SHA-256 that delegates
|
|
9
|
+
* to `CryptoUtil.sha256Hex`.
|
|
10
|
+
*
|
|
11
|
+
* **Not a credential, and not a secret.** It is one unsalted SHA-256, so it is
|
|
12
|
+
* exactly as crackable as the legacy unsalted digest it is sometimes confused
|
|
13
|
+
* with — a rainbow table recovers it instantly, and two users who chose the same
|
|
14
|
+
* password share a value. It exists to be a memo *key* and nothing else. Any
|
|
15
|
+
* code that treats it as a password hash is wrong.
|
|
16
|
+
*
|
|
17
|
+
* ## Why it exists
|
|
18
|
+
*
|
|
19
|
+
* Password verification is memoized so a repeated request does not pay a PBKDF2
|
|
20
|
+
* derivation again. The memo must distinguish "this exact password was already
|
|
21
|
+
* verified against this stored hash" from "someone presented a different password
|
|
22
|
+
* for this username". Keying on `username` alone would accept **any** password
|
|
23
|
+
* for a recently-used username — a total authentication bypass. The lookup key
|
|
24
|
+
* therefore includes this fingerprint: it makes the key specific to the password
|
|
25
|
+
* actually presented, at the cost of one SHA-256 (microseconds) instead of one
|
|
26
|
+
* PBKDF2 derivation (milliseconds, and on the Workers Free plan more than the
|
|
27
|
+
* entire CPU budget).
|
|
28
|
+
*/
|
|
29
|
+
async function passwordFingerprint(password) {
|
|
30
|
+
return CryptoUtil.sha256Hex(password);
|
|
31
|
+
}
|
|
32
|
+
export { passwordFingerprint };
|
|
33
|
+
//# sourceMappingURL=PasswordFingerprint.js.map
|