@growth-labs/auth 0.2.4 → 0.3.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/dist/routes/logout.d.ts +1 -0
- package/dist/routes/logout.d.ts.map +1 -1
- package/dist/routes/logout.js +10 -0
- package/dist/routes/logout.js.map +1 -1
- package/dist/utils/identity.d.ts +109 -0
- package/dist/utils/identity.d.ts.map +1 -0
- package/dist/utils/identity.js +159 -0
- package/dist/utils/identity.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +2 -2
package/dist/routes/logout.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../src/routes/logout.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../src/routes/logout.ts"],"names":[],"mappings":"AAKA,UAAU,aAAa;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,GAAG,CAAA;IACR,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,wBAAsB,GAAG,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoCnE"}
|
package/dist/routes/logout.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getConfig } from '../config-store.js';
|
|
2
|
+
import { analyticsAvailable, emitAuthEvent } from '../utils/analytics.js';
|
|
2
3
|
import { clearCookie, clearTokenCookies, getCookie } from '../utils/cookies.js';
|
|
3
4
|
import { revokeToken } from '../utils/openauth.js';
|
|
4
5
|
export async function GET(context) {
|
|
@@ -10,6 +11,15 @@ export async function GET(context) {
|
|
|
10
11
|
const promise = revokeToken(refreshToken, clientId);
|
|
11
12
|
context.waitUntil?.(promise);
|
|
12
13
|
}
|
|
14
|
+
// Emit auth_logout (non-blocking) when the consumer has @growth-labs/analytics
|
|
15
|
+
// wired. Without analytics installed this is a no-op (analyticsAvailable
|
|
16
|
+
// returns false). Documented in packages-docs/event-registry.md.
|
|
17
|
+
if (analyticsAvailable(context)) {
|
|
18
|
+
const promise = emitAuthEvent(context, 'auth_logout', {
|
|
19
|
+
revoked: refreshToken ? 'true' : 'false',
|
|
20
|
+
});
|
|
21
|
+
context.waitUntil?.(promise);
|
|
22
|
+
}
|
|
13
23
|
// Build redirect response
|
|
14
24
|
const redirectUrl = new URL(options.logoutRedirect, context.url.origin).toString();
|
|
15
25
|
const response = Response.redirect(redirectUrl, 302);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/routes/logout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/routes/logout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACzE,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AASlD,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,OAAsB;IAC/C,MAAM,OAAO,GAAG,SAAS,EAAE,CAAA;IAC3B,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,KAAK,CAAC,CAAA;IAC7E,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAA;IAEzD,kCAAkC;IAClC,IAAI,YAAY,EAAE,CAAC;QAClB,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAA;QACnD,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAA;IAC7B,CAAC;IAED,+EAA+E;IAC/E,yEAAyE;IACzE,iEAAiE;IACjE,IAAI,kBAAkB,CAAC,OAA+C,CAAC,EAAE,CAAC;QACzE,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE;YACrD,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;SACxC,CAAC,CAAA;QACF,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAA;IAC7B,CAAC;IAED,0BAA0B;IAC1B,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAA;IAClF,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;IAEpD,oCAAoC;IACpC,MAAM,eAAe,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;QACnD,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,OAAO,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;KACtC,CAAC,CAAA;IAEF,iBAAiB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAA;IAC3C,WAAW,CAAC,eAAe,EAAE,GAAG,OAAO,CAAC,YAAY,OAAO,CAAC,CAAA;IAE5D,OAAO,eAAe,CAAA;AACvB,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The literal prefix the issuer emits on every `sub` claim. Pinned as
|
|
3
|
+
* the contract for this package's auth flow. Any change is breaking
|
|
4
|
+
* and triggers a major bump.
|
|
5
|
+
*/
|
|
6
|
+
export declare const IDENTITY_SUBJECT_PREFIX = "user:";
|
|
7
|
+
/** Stable error code for sub-format drift. */
|
|
8
|
+
export declare const IDENTITY_SUB_FORMAT_DRIFT = "IDENTITY_SUB_FORMAT_DRIFT";
|
|
9
|
+
export declare class IdentitySubFormatDriftError extends Error {
|
|
10
|
+
readonly observedSubject: string;
|
|
11
|
+
readonly code = "IDENTITY_SUB_FORMAT_DRIFT";
|
|
12
|
+
constructor(message: string, observedSubject: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate and unwrap a `sub` claim. Returns the bare ULID portion
|
|
16
|
+
* (the value Fronts stores in `users.identity_user_id` post-migration
|
|
17
|
+
* is the prefixed form; the bare ULID is the convenient handle for
|
|
18
|
+
* matching against business-side records that pre-date the prefix).
|
|
19
|
+
*
|
|
20
|
+
* Throws `IdentitySubFormatDriftError` when the subject does not match
|
|
21
|
+
* `user:<ULID>`. The error code `IDENTITY_SUB_FORMAT_DRIFT` is stable
|
|
22
|
+
* for observability matching.
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractIdentityId(sub: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Convenience: normalize either form to the canonical prefixed `sub`.
|
|
27
|
+
* Idempotent on already-prefixed input. Throws on invalid ULID.
|
|
28
|
+
*
|
|
29
|
+
* Useful inside a one-shot migration: pass each row's stored
|
|
30
|
+
* `identity_user_id` through this and write back the result.
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeIdentitySubject(value: string): string;
|
|
33
|
+
export interface BridgeMigrationOptions {
|
|
34
|
+
/** Table that holds the bridge column. Default: `users`. */
|
|
35
|
+
table?: string;
|
|
36
|
+
/** Column that stores the issuer subject. Default: `identity_user_id`. */
|
|
37
|
+
column?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface BridgeMigrationStatement {
|
|
40
|
+
sql: string;
|
|
41
|
+
bindings?: readonly unknown[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the read-only canary query consumers run before deploying the
|
|
45
|
+
* issuer subject-format change. Returns the count of rows that hold
|
|
46
|
+
* legacy unprefixed values. A non-zero count means some logins will
|
|
47
|
+
* throw `IdentityBridgeConflictError` until those rows are rewritten
|
|
48
|
+
* by `buildLegacyIdentitySubjectMigration`.
|
|
49
|
+
*
|
|
50
|
+
* ```sql
|
|
51
|
+
* SELECT COUNT(*) FROM users WHERE identity_user_id IS NOT NULL
|
|
52
|
+
* AND identity_user_id NOT LIKE 'user:%';
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildLegacyIdentitySubjectCanary(options?: BridgeMigrationOptions): BridgeMigrationStatement;
|
|
56
|
+
/**
|
|
57
|
+
* Build the rewrite statement that prefixes legacy bare-ULID values to
|
|
58
|
+
* the canonical `user:<ULID>` form. Idempotent: rows whose value
|
|
59
|
+
* already starts with `user:` are not touched. Run as a one-shot
|
|
60
|
+
* migration BEFORE the issuer subject-format change goes live.
|
|
61
|
+
*
|
|
62
|
+
* ```sql
|
|
63
|
+
* UPDATE users
|
|
64
|
+
* SET identity_user_id = 'user:' || identity_user_id
|
|
65
|
+
* WHERE identity_user_id IS NOT NULL
|
|
66
|
+
* AND identity_user_id NOT LIKE 'user:%';
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* D1 / SQLite syntax. Adapt the concatenation operator (`||`) for
|
|
70
|
+
* other engines if you fork.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildLegacyIdentitySubjectMigration(options?: BridgeMigrationOptions): BridgeMigrationStatement;
|
|
73
|
+
/**
|
|
74
|
+
* D1-binding-compatible: count legacy rows. Designed for consumer
|
|
75
|
+
* code like:
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* const count = await findLegacyIdentitySubjects(env.SITE_DB)
|
|
79
|
+
* if (count > 0) throw new Error(`${count} rows need migration`)
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
interface D1Like {
|
|
83
|
+
prepare(sql: string): {
|
|
84
|
+
first<T = unknown>(): Promise<T | null>;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export declare function findLegacyIdentitySubjects(db: D1Like, options?: BridgeMigrationOptions): Promise<number>;
|
|
88
|
+
/**
|
|
89
|
+
* D1-binding-compatible: rewrite legacy rows in place. Returns the
|
|
90
|
+
* number of rows updated.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* const rewritten = await rewriteLegacyIdentitySubjects(env.SITE_DB)
|
|
94
|
+
* console.log(`Migrated ${rewritten} rows`)
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
interface D1RunLike {
|
|
98
|
+
prepare(sql: string): {
|
|
99
|
+
run(): Promise<{
|
|
100
|
+
meta?: {
|
|
101
|
+
changes?: number;
|
|
102
|
+
rows_written?: number;
|
|
103
|
+
};
|
|
104
|
+
}>;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export declare function rewriteLegacyIdentitySubjects(db: D1RunLike, options?: BridgeMigrationOptions): Promise<number>;
|
|
108
|
+
export {};
|
|
109
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/utils/identity.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,UAAU,CAAA;AAS9C,8CAA8C;AAC9C,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AAEpE,qBAAa,2BAA4B,SAAQ,KAAK;IAIpD,QAAQ,CAAC,eAAe,EAAE,MAAM;IAHjC,QAAQ,CAAC,IAAI,+BAA4B;gBAExC,OAAO,EAAE,MAAM,EACN,eAAe,EAAE,MAAM;CAKjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAsBrD;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAqB9D;AAID,MAAM,WAAW,sBAAsB;IACtC,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,wBAAwB;IACxC,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gCAAgC,CAC/C,OAAO,GAAE,sBAA2B,GAClC,wBAAwB,CAO1B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mCAAmC,CAClD,OAAO,GAAE,sBAA2B,GAClC,wBAAwB,CAS1B;AAED;;;;;;;;GAQG;AACH,UAAU,MAAM;IACf,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QACrB,KAAK,CAAC,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;KACvC,CAAA;CACD;AAED,wBAAsB,0BAA0B,CAC/C,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,sBAA2B,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;;;;;;;GAQG;AACH,UAAU,SAAS;IAClB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QACrB,GAAG,IAAI,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,OAAO,CAAC,EAAE,MAAM,CAAC;gBAAC,YAAY,CAAC,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,CAAC,CAAA;KACtE,CAAA;CACD;AAED,wBAAsB,6BAA6B,CAClD,EAAE,EAAE,SAAS,EACb,OAAO,GAAE,sBAA2B,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Identity-bridge helpers. The OpenAuth issuer this package targets emits
|
|
2
|
+
// `sub` claims in the form `user:<ULID>` (e.g. `user:01KP8KA31Z3AGRCC578R9HXW0H`).
|
|
3
|
+
// The unprefixed bare-ULID form was used in early issuer releases and is
|
|
4
|
+
// no longer the contract.
|
|
5
|
+
//
|
|
6
|
+
// Two scenarios this module addresses:
|
|
7
|
+
//
|
|
8
|
+
// 1. **Sub-format drift detection.** A consumer's auth code that pulls
|
|
9
|
+
// the identity ID from a JWT should fail loudly if the issuer emits
|
|
10
|
+
// something that doesn't match the documented contract. Use
|
|
11
|
+
// `extractIdentityId` for the canonical extract-and-validate path.
|
|
12
|
+
//
|
|
13
|
+
// 2. **Legacy-row bridge migration.** A consumer that started using the
|
|
14
|
+
// issuer before the prefix change has rows in their `users` table
|
|
15
|
+
// whose `identity_user_id` column holds the bare ULID. After the
|
|
16
|
+
// issuer migration, every login by those users tries to bridge the
|
|
17
|
+
// new `user:<ULID>` to a row that holds the old `<ULID>` and throws
|
|
18
|
+
// `IdentityBridgeConflictError` from the consumer's bridge layer.
|
|
19
|
+
// `buildLegacyIdentitySubjectMigration` returns a parameterized SQL
|
|
20
|
+
// statement set the consumer can run as a one-shot data migration
|
|
21
|
+
// BEFORE deploying the issuer change. `findLegacyIdentitySubjects`
|
|
22
|
+
// is a canary query consumers can run any time to count remaining
|
|
23
|
+
// legacy rows.
|
|
24
|
+
/**
|
|
25
|
+
* The literal prefix the issuer emits on every `sub` claim. Pinned as
|
|
26
|
+
* the contract for this package's auth flow. Any change is breaking
|
|
27
|
+
* and triggers a major bump.
|
|
28
|
+
*/
|
|
29
|
+
export const IDENTITY_SUBJECT_PREFIX = 'user:';
|
|
30
|
+
/**
|
|
31
|
+
* Documented ULID shape: 26 chars of Crockford base32. The issuer
|
|
32
|
+
* uses ULIDv1 and never falls back to a generic UUID — pin the
|
|
33
|
+
* stricter pattern so drift is caught early.
|
|
34
|
+
*/
|
|
35
|
+
const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
36
|
+
/** Stable error code for sub-format drift. */
|
|
37
|
+
export const IDENTITY_SUB_FORMAT_DRIFT = 'IDENTITY_SUB_FORMAT_DRIFT';
|
|
38
|
+
export class IdentitySubFormatDriftError extends Error {
|
|
39
|
+
observedSubject;
|
|
40
|
+
code = IDENTITY_SUB_FORMAT_DRIFT;
|
|
41
|
+
constructor(message, observedSubject) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.observedSubject = observedSubject;
|
|
44
|
+
this.name = 'IdentitySubFormatDriftError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Validate and unwrap a `sub` claim. Returns the bare ULID portion
|
|
49
|
+
* (the value Fronts stores in `users.identity_user_id` post-migration
|
|
50
|
+
* is the prefixed form; the bare ULID is the convenient handle for
|
|
51
|
+
* matching against business-side records that pre-date the prefix).
|
|
52
|
+
*
|
|
53
|
+
* Throws `IdentitySubFormatDriftError` when the subject does not match
|
|
54
|
+
* `user:<ULID>`. The error code `IDENTITY_SUB_FORMAT_DRIFT` is stable
|
|
55
|
+
* for observability matching.
|
|
56
|
+
*/
|
|
57
|
+
export function extractIdentityId(sub) {
|
|
58
|
+
if (typeof sub !== 'string' || sub.length === 0) {
|
|
59
|
+
throw new IdentitySubFormatDriftError('JWT sub claim is missing or not a string', String(sub));
|
|
60
|
+
}
|
|
61
|
+
if (!sub.startsWith(IDENTITY_SUBJECT_PREFIX)) {
|
|
62
|
+
throw new IdentitySubFormatDriftError(`JWT sub claim "${sub}" is missing the required "${IDENTITY_SUBJECT_PREFIX}" prefix. ` +
|
|
63
|
+
`The issuer contract pins the format as "${IDENTITY_SUBJECT_PREFIX}<ULID>". ` +
|
|
64
|
+
'If your issuer was migrated from an older release, see ' +
|
|
65
|
+
'packages-docs/auth-identity-bridge.md for the bridge-row migration helper.', sub);
|
|
66
|
+
}
|
|
67
|
+
const id = sub.slice(IDENTITY_SUBJECT_PREFIX.length);
|
|
68
|
+
if (!ULID_PATTERN.test(id)) {
|
|
69
|
+
throw new IdentitySubFormatDriftError(`JWT sub claim "${sub}" has the "${IDENTITY_SUBJECT_PREFIX}" prefix but the suffix ` +
|
|
70
|
+
'is not a valid 26-char Crockford base32 ULID.', sub);
|
|
71
|
+
}
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Convenience: normalize either form to the canonical prefixed `sub`.
|
|
76
|
+
* Idempotent on already-prefixed input. Throws on invalid ULID.
|
|
77
|
+
*
|
|
78
|
+
* Useful inside a one-shot migration: pass each row's stored
|
|
79
|
+
* `identity_user_id` through this and write back the result.
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeIdentitySubject(value) {
|
|
82
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
83
|
+
throw new IdentitySubFormatDriftError('value is missing or not a string', String(value));
|
|
84
|
+
}
|
|
85
|
+
if (value.startsWith(IDENTITY_SUBJECT_PREFIX)) {
|
|
86
|
+
const id = value.slice(IDENTITY_SUBJECT_PREFIX.length);
|
|
87
|
+
if (!ULID_PATTERN.test(id)) {
|
|
88
|
+
throw new IdentitySubFormatDriftError(`value "${value}" has the "${IDENTITY_SUBJECT_PREFIX}" prefix but the suffix is not a valid ULID.`, value);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
if (!ULID_PATTERN.test(value)) {
|
|
93
|
+
throw new IdentitySubFormatDriftError(`value "${value}" is neither a prefixed "${IDENTITY_SUBJECT_PREFIX}<ULID>" nor a valid bare ULID.`, value);
|
|
94
|
+
}
|
|
95
|
+
return `${IDENTITY_SUBJECT_PREFIX}${value}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the read-only canary query consumers run before deploying the
|
|
99
|
+
* issuer subject-format change. Returns the count of rows that hold
|
|
100
|
+
* legacy unprefixed values. A non-zero count means some logins will
|
|
101
|
+
* throw `IdentityBridgeConflictError` until those rows are rewritten
|
|
102
|
+
* by `buildLegacyIdentitySubjectMigration`.
|
|
103
|
+
*
|
|
104
|
+
* ```sql
|
|
105
|
+
* SELECT COUNT(*) FROM users WHERE identity_user_id IS NOT NULL
|
|
106
|
+
* AND identity_user_id NOT LIKE 'user:%';
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function buildLegacyIdentitySubjectCanary(options = {}) {
|
|
110
|
+
const { table = 'users', column = 'identity_user_id' } = options;
|
|
111
|
+
assertSafeSqlIdentifier(table);
|
|
112
|
+
assertSafeSqlIdentifier(column);
|
|
113
|
+
return {
|
|
114
|
+
sql: `SELECT COUNT(*) AS legacy_count FROM ${table} WHERE ${column} IS NOT NULL AND ${column} NOT LIKE 'user:%'`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Build the rewrite statement that prefixes legacy bare-ULID values to
|
|
119
|
+
* the canonical `user:<ULID>` form. Idempotent: rows whose value
|
|
120
|
+
* already starts with `user:` are not touched. Run as a one-shot
|
|
121
|
+
* migration BEFORE the issuer subject-format change goes live.
|
|
122
|
+
*
|
|
123
|
+
* ```sql
|
|
124
|
+
* UPDATE users
|
|
125
|
+
* SET identity_user_id = 'user:' || identity_user_id
|
|
126
|
+
* WHERE identity_user_id IS NOT NULL
|
|
127
|
+
* AND identity_user_id NOT LIKE 'user:%';
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* D1 / SQLite syntax. Adapt the concatenation operator (`||`) for
|
|
131
|
+
* other engines if you fork.
|
|
132
|
+
*/
|
|
133
|
+
export function buildLegacyIdentitySubjectMigration(options = {}) {
|
|
134
|
+
const { table = 'users', column = 'identity_user_id' } = options;
|
|
135
|
+
assertSafeSqlIdentifier(table);
|
|
136
|
+
assertSafeSqlIdentifier(column);
|
|
137
|
+
return {
|
|
138
|
+
sql: `UPDATE ${table} SET ${column} = 'user:' || ${column} ` +
|
|
139
|
+
`WHERE ${column} IS NOT NULL AND ${column} NOT LIKE 'user:%'`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export async function findLegacyIdentitySubjects(db, options = {}) {
|
|
143
|
+
const { sql } = buildLegacyIdentitySubjectCanary(options);
|
|
144
|
+
const row = await db.prepare(sql).first();
|
|
145
|
+
return row?.legacy_count ?? 0;
|
|
146
|
+
}
|
|
147
|
+
export async function rewriteLegacyIdentitySubjects(db, options = {}) {
|
|
148
|
+
const { sql } = buildLegacyIdentitySubjectMigration(options);
|
|
149
|
+
const result = await db.prepare(sql).run();
|
|
150
|
+
return result?.meta?.changes ?? result?.meta?.rows_written ?? 0;
|
|
151
|
+
}
|
|
152
|
+
// ─── Internal ───
|
|
153
|
+
function assertSafeSqlIdentifier(value) {
|
|
154
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
155
|
+
throw new Error(`@growth-labs/auth: identifier "${value}" is not a safe SQL identifier ` +
|
|
156
|
+
'(letters, digits, underscores; must not start with a digit).');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/utils/identity.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { emitAuthEvent, trackAuthEvent } from './analytics.js';
|
|
2
|
+
export { type BridgeMigrationOptions, type BridgeMigrationStatement, buildLegacyIdentitySubjectCanary, buildLegacyIdentitySubjectMigration, extractIdentityId, findLegacyIdentitySubjects, IDENTITY_SUB_FORMAT_DRIFT, IDENTITY_SUBJECT_PREFIX, IdentitySubFormatDriftError, normalizeIdentitySubject, rewriteLegacyIdentitySubjects, } from './identity.js';
|
|
2
3
|
export { getAuthorizationUrl, getMagicLinkUrl, getPasswordLoginUrl } from './openauth.js';
|
|
3
4
|
export { generatePKCE } from './pkce.js';
|
|
4
5
|
export { generateState, verifyState } from './state.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EACN,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,mCAAmC,EACnC,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,GAC7B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/utils/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { emitAuthEvent, trackAuthEvent } from './analytics.js';
|
|
2
|
+
export { buildLegacyIdentitySubjectCanary, buildLegacyIdentitySubjectMigration, extractIdentityId, findLegacyIdentitySubjects, IDENTITY_SUB_FORMAT_DRIFT, IDENTITY_SUBJECT_PREFIX, IdentitySubFormatDriftError, normalizeIdentitySubject, rewriteLegacyIdentitySubjects, } from './identity.js';
|
|
2
3
|
export { getAuthorizationUrl, getMagicLinkUrl, getPasswordLoginUrl } from './openauth.js';
|
|
3
4
|
export { generatePKCE } from './pkce.js';
|
|
4
5
|
export { generateState, verifyState } from './state.js';
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAGN,gCAAgC,EAChC,mCAAmC,EACnC,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,GAC7B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-labs/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"astro": "^6.0.0",
|
|
53
53
|
"typescript": "^5.8.0",
|
|
54
54
|
"vitest": "^3.0.0",
|
|
55
|
-
"@growth-labs/analytics": "0.2.
|
|
55
|
+
"@growth-labs/analytics": "0.2.2"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsc",
|