@ankhorage/auth-supabase 0.0.0-stage → 2.0.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -2
  3. package/dist/authConfiguration.d.ts +15 -0
  4. package/dist/authConfiguration.d.ts.map +1 -0
  5. package/dist/authConfiguration.js +69 -0
  6. package/dist/authConfiguration.js.map +1 -0
  7. package/dist/authPasswordOperations.d.ts +10 -0
  8. package/dist/authPasswordOperations.d.ts.map +1 -0
  9. package/dist/authPasswordOperations.js +163 -0
  10. package/dist/authPasswordOperations.js.map +1 -0
  11. package/dist/authSessionContext.d.ts +19 -0
  12. package/dist/authSessionContext.d.ts.map +1 -0
  13. package/dist/authSessionContext.js +96 -0
  14. package/dist/authSessionContext.js.map +1 -0
  15. package/dist/authSessionOperations.d.ts +7 -0
  16. package/dist/authSessionOperations.d.ts.map +1 -0
  17. package/dist/authSessionOperations.js +79 -0
  18. package/dist/authSessionOperations.js.map +1 -0
  19. package/dist/createSupabaseAuthAdapter.d.ts +5 -0
  20. package/dist/createSupabaseAuthAdapter.d.ts.map +1 -0
  21. package/dist/createSupabaseAuthAdapter.js +76 -0
  22. package/dist/createSupabaseAuthAdapter.js.map +1 -0
  23. package/dist/errors.d.ts +7 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +89 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +6 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/oauth.d.ts +5 -0
  32. package/dist/oauth.d.ts.map +1 -0
  33. package/dist/oauth.js +34 -0
  34. package/dist/oauth.js.map +1 -0
  35. package/dist/oauthAttemptId.d.ts +9 -0
  36. package/dist/oauthAttemptId.d.ts.map +1 -0
  37. package/dist/oauthAttemptId.js +51 -0
  38. package/dist/oauthAttemptId.js.map +1 -0
  39. package/dist/oauthCallback.d.ts +23 -0
  40. package/dist/oauthCallback.d.ts.map +1 -0
  41. package/dist/oauthCallback.js +81 -0
  42. package/dist/oauthCallback.js.map +1 -0
  43. package/dist/oauthComplete.d.ts +5 -0
  44. package/dist/oauthComplete.d.ts.map +1 -0
  45. package/dist/oauthComplete.js +133 -0
  46. package/dist/oauthComplete.js.map +1 -0
  47. package/dist/oauthCompletionState.d.ts +13 -0
  48. package/dist/oauthCompletionState.d.ts.map +1 -0
  49. package/dist/oauthCompletionState.js +25 -0
  50. package/dist/oauthCompletionState.js.map +1 -0
  51. package/dist/oauthErrors.d.ts +9 -0
  52. package/dist/oauthErrors.d.ts.map +1 -0
  53. package/dist/oauthErrors.js +87 -0
  54. package/dist/oauthErrors.js.map +1 -0
  55. package/dist/oauthExchange.d.ts +14 -0
  56. package/dist/oauthExchange.d.ts.map +1 -0
  57. package/dist/oauthExchange.js +100 -0
  58. package/dist/oauthExchange.js.map +1 -0
  59. package/dist/oauthLifecycle.d.ts +8 -0
  60. package/dist/oauthLifecycle.d.ts.map +1 -0
  61. package/dist/oauthLifecycle.js +24 -0
  62. package/dist/oauthLifecycle.js.map +1 -0
  63. package/dist/oauthProviderDefinitions.d.ts +77 -0
  64. package/dist/oauthProviderDefinitions.d.ts.map +1 -0
  65. package/dist/oauthProviderDefinitions.js +114 -0
  66. package/dist/oauthProviderDefinitions.js.map +1 -0
  67. package/dist/oauthRuntime.d.ts +23 -0
  68. package/dist/oauthRuntime.d.ts.map +1 -0
  69. package/dist/oauthRuntime.js +22 -0
  70. package/dist/oauthRuntime.js.map +1 -0
  71. package/dist/oauthSetupRequirements.d.ts +12 -0
  72. package/dist/oauthSetupRequirements.d.ts.map +1 -0
  73. package/dist/oauthSetupRequirements.js +39 -0
  74. package/dist/oauthSetupRequirements.js.map +1 -0
  75. package/dist/oauthStart.d.ts +4 -0
  76. package/dist/oauthStart.d.ts.map +1 -0
  77. package/dist/oauthStart.js +108 -0
  78. package/dist/oauthStart.js.map +1 -0
  79. package/dist/oauthStartSupport.d.ts +52 -0
  80. package/dist/oauthStartSupport.d.ts.map +1 -0
  81. package/dist/oauthStartSupport.js +137 -0
  82. package/dist/oauthStartSupport.js.map +1 -0
  83. package/dist/oauthStorage.d.ts +25 -0
  84. package/dist/oauthStorage.d.ts.map +1 -0
  85. package/dist/oauthStorage.js +146 -0
  86. package/dist/oauthStorage.js.map +1 -0
  87. package/dist/oauthTypes.d.ts +49 -0
  88. package/dist/oauthTypes.d.ts.map +1 -0
  89. package/dist/oauthTypes.js +6 -0
  90. package/dist/oauthTypes.js.map +1 -0
  91. package/dist/profileVerification.d.ts +24 -0
  92. package/dist/profileVerification.d.ts.map +1 -0
  93. package/dist/profileVerification.js +178 -0
  94. package/dist/profileVerification.js.map +1 -0
  95. package/dist/session.d.ts +6 -0
  96. package/dist/session.d.ts.map +1 -0
  97. package/dist/session.js +124 -0
  98. package/dist/session.js.map +1 -0
  99. package/dist/types.d.ts +63 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +8 -0
  102. package/dist/types.js.map +1 -0
  103. package/package.json +70 -4
@@ -0,0 +1,25 @@
1
+ import { type StoredConsumedOAuthCallback, type StoredOAuthAttempt, type StoredOAuthAttemptReadResult } from './oauthTypes.js';
2
+ import type { SupabaseAuthStorage } from './types.js';
3
+ /*** Reads and validates the persisted OAuth authorization attempt. */
4
+ export declare function readAttempt(storage: SupabaseAuthStorage, key: string): Promise<StoredOAuthAttemptReadResult>;
5
+ /*** Persists the current OAuth authorization attempt. */
6
+ export declare function writeAttempt(storage: SupabaseAuthStorage, key: string, attempt: StoredOAuthAttempt): Promise<void>;
7
+ /*** Finalizes an OAuth attempt and removes its one-use PKCE verifier. */
8
+ export declare function finalizeAttempt(storage: SupabaseAuthStorage, attemptKey: string, verifierKey: string, attempt: StoredOAuthAttempt, callbackFingerprint?: string): Promise<void>;
9
+ /*** Removes all persisted state for the active OAuth attempt. */
10
+ export declare function clearOAuthAttemptState(storage: SupabaseAuthStorage, attemptKey: string, verifierKey: string): Promise<void>;
11
+ /*** Reports whether an OAuth attempt has reached its expiry time. */
12
+ export declare function isAttemptExpired(attempt: StoredOAuthAttempt, now: number): boolean;
13
+ /*** Fingerprints one callback for exact replay detection within its attempt. */
14
+ export declare function fingerprintOAuthCallback(attemptId: string, code: string): string;
15
+ /*** Fingerprints one callback across attempts sharing the same redirect URI. */
16
+ export declare function fingerprintConsumedOAuthCallback(redirectUri: string, code: string): string;
17
+ /*** Reads valid, unexpired callback fingerprints from persistent storage. */
18
+ export declare function readConsumedCallbacks(storage: SupabaseAuthStorage, key: string, currentTime: number): Promise<readonly StoredConsumedOAuthCallback[]>;
19
+ /*** Persists the bounded callback replay-protection ledger. */
20
+ export declare function writeConsumedCallbacks(storage: SupabaseAuthStorage, key: string, callbacks: readonly StoredConsumedOAuthCallback[]): Promise<void>;
21
+ /*** Compares equal-length callback fingerprints without data-dependent early exit. */
22
+ export declare function constantTimeEqual(left: string, right: string): boolean;
23
+ /*** Normalizes the configured OAuth attempt lifetime. */
24
+ export declare function normalizeAttemptLifetime(value: number | undefined): number;
25
+ //# sourceMappingURL=oauthStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthStorage.d.ts","sourceRoot":"","sources":["../src/oauthStorage.ts"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,2BAA2B,EAEhC,KAAK,kBAAkB,EACvB,KAAK,4BAA4B,EAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,sEAAsE;AACtE,wBAAsB,WAAW,CAC/B,OAAO,EAAE,mBAAmB,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,4BAA4B,CAAC,CASvC;AAED,yDAAyD;AACzD,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,EAC5B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,yEAAyE;AACzE,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAC3B,mBAAmB,CAAC,EAAE,MAAM,GAC3B,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,iEAAiE;AACjE,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,mBAAmB,EAC5B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAElF;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,gFAAgF;AAChF,wBAAgB,gCAAgC,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1F;AAED,6EAA6E;AAC7E,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,mBAAmB,EAC5B,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,SAAS,2BAA2B,EAAE,CAAC,CAQjD;AAED,+DAA+D;AAC/D,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,mBAAmB,EAC5B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,SAAS,2BAA2B,EAAE,GAChD,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAOtE;AAED,yDAAyD;AACzD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAM1E"}
@@ -0,0 +1,146 @@
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
3
+ import { isSupabaseOAuthProviderId } from './oauthProviderDefinitions.js';
4
+ import { ATTEMPT_VERSION, CONSUMED_CALLBACKS_VERSION, DEFAULT_ATTEMPT_LIFETIME_MS, MAX_CONSUMED_CALLBACKS, } from './oauthTypes.js';
5
+ /*** Reads and validates the persisted OAuth authorization attempt. */
6
+ export async function readAttempt(storage, key) {
7
+ const stored = await storage.getItem(key);
8
+ if (stored === null)
9
+ return { type: 'missing' };
10
+ try {
11
+ const parsed = JSON.parse(stored);
12
+ return isStoredAttempt(parsed) ? { type: 'valid', attempt: parsed } : { type: 'invalid' };
13
+ }
14
+ catch {
15
+ return { type: 'invalid' };
16
+ }
17
+ }
18
+ /*** Persists the current OAuth authorization attempt. */
19
+ export async function writeAttempt(storage, key, attempt) {
20
+ await storage.setItem(key, JSON.stringify(attempt));
21
+ }
22
+ /*** Finalizes an OAuth attempt and removes its one-use PKCE verifier. */
23
+ export async function finalizeAttempt(storage, attemptKey, verifierKey, attempt, callbackFingerprint) {
24
+ try {
25
+ await writeAttempt(storage, attemptKey, {
26
+ ...attempt,
27
+ status: 'completed',
28
+ ...(callbackFingerprint === undefined ? {} : { callbackFingerprint }),
29
+ });
30
+ }
31
+ catch {
32
+ await safeRemove(storage, attemptKey);
33
+ }
34
+ await safeRemove(storage, verifierKey);
35
+ }
36
+ /*** Removes all persisted state for the active OAuth attempt. */
37
+ export async function clearOAuthAttemptState(storage, attemptKey, verifierKey) {
38
+ await safeRemove(storage, attemptKey);
39
+ await safeRemove(storage, verifierKey);
40
+ }
41
+ /*** Reports whether an OAuth attempt has reached its expiry time. */
42
+ export function isAttemptExpired(attempt, now) {
43
+ return attempt.expiresAt <= now;
44
+ }
45
+ /*** Fingerprints one callback for exact replay detection within its attempt. */
46
+ export function fingerprintOAuthCallback(attemptId, code) {
47
+ return digest(`${attemptId}\u0000${code}`);
48
+ }
49
+ /*** Fingerprints one callback across attempts sharing the same redirect URI. */
50
+ export function fingerprintConsumedOAuthCallback(redirectUri, code) {
51
+ return digest(`${redirectUri}\u0000${code}`);
52
+ }
53
+ /*** Reads valid, unexpired callback fingerprints from persistent storage. */
54
+ export async function readConsumedCallbacks(storage, key, currentTime) {
55
+ const stored = await storage.getItem(key);
56
+ if (stored === null)
57
+ return [];
58
+ const parsed = JSON.parse(stored);
59
+ if (!isStoredConsumedOAuthCallbacks(parsed)) {
60
+ throw new TypeError('Persisted consumed OAuth callback state is invalid.');
61
+ }
62
+ return parsed.callbacks.filter((entry) => entry.expiresAt > currentTime);
63
+ }
64
+ /*** Persists the bounded callback replay-protection ledger. */
65
+ export async function writeConsumedCallbacks(storage, key, callbacks) {
66
+ const state = {
67
+ version: CONSUMED_CALLBACKS_VERSION,
68
+ callbacks: callbacks.slice(-MAX_CONSUMED_CALLBACKS),
69
+ };
70
+ await storage.setItem(key, JSON.stringify(state));
71
+ }
72
+ /*** Compares equal-length callback fingerprints without data-dependent early exit. */
73
+ export function constantTimeEqual(left, right) {
74
+ if (left.length !== right.length)
75
+ return false;
76
+ let difference = 0;
77
+ for (let index = 0; index < left.length; index += 1) {
78
+ difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
79
+ }
80
+ return difference === 0;
81
+ }
82
+ /*** Normalizes the configured OAuth attempt lifetime. */
83
+ export function normalizeAttemptLifetime(value) {
84
+ const lifetime = value ?? DEFAULT_ATTEMPT_LIFETIME_MS;
85
+ if (!Number.isSafeInteger(lifetime) || lifetime <= 0) {
86
+ throw new TypeError('OAuth authorization attempt lifetime must be a positive safe integer.');
87
+ }
88
+ return lifetime;
89
+ }
90
+ function isStoredAttempt(value) {
91
+ if (!isRecord(value))
92
+ return false;
93
+ return (hasAttemptIdentity(value) && hasAttemptTimeline(value) && hasValidCallbackFingerprint(value));
94
+ }
95
+ function hasAttemptIdentity(value) {
96
+ return (value.version === ATTEMPT_VERSION &&
97
+ typeof value.id === 'string' &&
98
+ isSupabaseOAuthProviderId(typeof value.provider === 'string' ? value.provider : '') &&
99
+ typeof value.redirectUri === 'string' &&
100
+ (value.status === 'pending' || value.status === 'completing' || value.status === 'completed'));
101
+ }
102
+ function hasAttemptTimeline(value) {
103
+ return (typeof value.createdAt === 'number' &&
104
+ Number.isSafeInteger(value.createdAt) &&
105
+ value.createdAt >= 0 &&
106
+ typeof value.expiresAt === 'number' &&
107
+ Number.isSafeInteger(value.expiresAt) &&
108
+ value.expiresAt > value.createdAt);
109
+ }
110
+ function hasValidCallbackFingerprint(value) {
111
+ if (value.callbackFingerprint === undefined)
112
+ return true;
113
+ return ((value.status === 'completing' || value.status === 'completed') &&
114
+ typeof value.callbackFingerprint === 'string' &&
115
+ /^[0-9a-f]{64}$/u.test(value.callbackFingerprint));
116
+ }
117
+ function isStoredConsumedOAuthCallbacks(value) {
118
+ return (isRecord(value) &&
119
+ value.version === CONSUMED_CALLBACKS_VERSION &&
120
+ Array.isArray(value.callbacks) &&
121
+ value.callbacks.length <= MAX_CONSUMED_CALLBACKS &&
122
+ value.callbacks.every(isStoredConsumedOAuthCallback));
123
+ }
124
+ function isStoredConsumedOAuthCallback(value) {
125
+ return (isRecord(value) &&
126
+ typeof value.fingerprint === 'string' &&
127
+ /^[0-9a-f]{64}$/u.test(value.fingerprint) &&
128
+ typeof value.expiresAt === 'number' &&
129
+ Number.isSafeInteger(value.expiresAt) &&
130
+ value.expiresAt > 0);
131
+ }
132
+ async function safeRemove(storage, key) {
133
+ try {
134
+ await storage.removeItem(key);
135
+ }
136
+ catch {
137
+ // Cleanup failure is intentionally not exposed with storage contents or secret values.
138
+ }
139
+ }
140
+ function digest(value) {
141
+ return bytesToHex(sha256(utf8ToBytes(value)));
142
+ }
143
+ function isRecord(value) {
144
+ return typeof value === 'object' && value !== null;
145
+ }
146
+ //# sourceMappingURL=oauthStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthStorage.js","sourceRoot":"","sources":["../src/oauthStorage.ts"],"names":[],"mappings":"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","sourcesContent":["import { sha256 } from '@noble/hashes/sha2.js';\nimport { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';\n\nimport { isSupabaseOAuthProviderId } from './oauthProviderDefinitions.js';\nimport {\n ATTEMPT_VERSION,\n CONSUMED_CALLBACKS_VERSION,\n DEFAULT_ATTEMPT_LIFETIME_MS,\n MAX_CONSUMED_CALLBACKS,\n type StoredConsumedOAuthCallback,\n type StoredConsumedOAuthCallbacks,\n type StoredOAuthAttempt,\n type StoredOAuthAttemptReadResult,\n} from './oauthTypes.js';\nimport type { SupabaseAuthStorage } from './types.js';\n\n/*** Reads and validates the persisted OAuth authorization attempt. */\nexport async function readAttempt(\n storage: SupabaseAuthStorage,\n key: string,\n): Promise<StoredOAuthAttemptReadResult> {\n const stored = await storage.getItem(key);\n if (stored === null) return { type: 'missing' };\n try {\n const parsed: unknown = JSON.parse(stored);\n return isStoredAttempt(parsed) ? { type: 'valid', attempt: parsed } : { type: 'invalid' };\n } catch {\n return { type: 'invalid' };\n }\n}\n\n/*** Persists the current OAuth authorization attempt. */\nexport async function writeAttempt(\n storage: SupabaseAuthStorage,\n key: string,\n attempt: StoredOAuthAttempt,\n): Promise<void> {\n await storage.setItem(key, JSON.stringify(attempt));\n}\n\n/*** Finalizes an OAuth attempt and removes its one-use PKCE verifier. */\nexport async function finalizeAttempt(\n storage: SupabaseAuthStorage,\n attemptKey: string,\n verifierKey: string,\n attempt: StoredOAuthAttempt,\n callbackFingerprint?: string,\n): Promise<void> {\n try {\n await writeAttempt(storage, attemptKey, {\n ...attempt,\n status: 'completed',\n ...(callbackFingerprint === undefined ? {} : { callbackFingerprint }),\n });\n } catch {\n await safeRemove(storage, attemptKey);\n }\n await safeRemove(storage, verifierKey);\n}\n\n/*** Removes all persisted state for the active OAuth attempt. */\nexport async function clearOAuthAttemptState(\n storage: SupabaseAuthStorage,\n attemptKey: string,\n verifierKey: string,\n): Promise<void> {\n await safeRemove(storage, attemptKey);\n await safeRemove(storage, verifierKey);\n}\n\n/*** Reports whether an OAuth attempt has reached its expiry time. */\nexport function isAttemptExpired(attempt: StoredOAuthAttempt, now: number): boolean {\n return attempt.expiresAt <= now;\n}\n\n/*** Fingerprints one callback for exact replay detection within its attempt. */\nexport function fingerprintOAuthCallback(attemptId: string, code: string): string {\n return digest(`${attemptId}\\u0000${code}`);\n}\n\n/*** Fingerprints one callback across attempts sharing the same redirect URI. */\nexport function fingerprintConsumedOAuthCallback(redirectUri: string, code: string): string {\n return digest(`${redirectUri}\\u0000${code}`);\n}\n\n/*** Reads valid, unexpired callback fingerprints from persistent storage. */\nexport async function readConsumedCallbacks(\n storage: SupabaseAuthStorage,\n key: string,\n currentTime: number,\n): Promise<readonly StoredConsumedOAuthCallback[]> {\n const stored = await storage.getItem(key);\n if (stored === null) return [];\n const parsed: unknown = JSON.parse(stored);\n if (!isStoredConsumedOAuthCallbacks(parsed)) {\n throw new TypeError('Persisted consumed OAuth callback state is invalid.');\n }\n return parsed.callbacks.filter((entry) => entry.expiresAt > currentTime);\n}\n\n/*** Persists the bounded callback replay-protection ledger. */\nexport async function writeConsumedCallbacks(\n storage: SupabaseAuthStorage,\n key: string,\n callbacks: readonly StoredConsumedOAuthCallback[],\n): Promise<void> {\n const state: StoredConsumedOAuthCallbacks = {\n version: CONSUMED_CALLBACKS_VERSION,\n callbacks: callbacks.slice(-MAX_CONSUMED_CALLBACKS),\n };\n await storage.setItem(key, JSON.stringify(state));\n}\n\n/*** Compares equal-length callback fingerprints without data-dependent early exit. */\nexport function constantTimeEqual(left: string, right: string): boolean {\n if (left.length !== right.length) return false;\n let difference = 0;\n for (let index = 0; index < left.length; index += 1) {\n difference |= left.charCodeAt(index) ^ right.charCodeAt(index);\n }\n return difference === 0;\n}\n\n/*** Normalizes the configured OAuth attempt lifetime. */\nexport function normalizeAttemptLifetime(value: number | undefined): number {\n const lifetime = value ?? DEFAULT_ATTEMPT_LIFETIME_MS;\n if (!Number.isSafeInteger(lifetime) || lifetime <= 0) {\n throw new TypeError('OAuth authorization attempt lifetime must be a positive safe integer.');\n }\n return lifetime;\n}\n\nfunction isStoredAttempt(value: unknown): value is StoredOAuthAttempt {\n if (!isRecord(value)) return false;\n return (\n hasAttemptIdentity(value) && hasAttemptTimeline(value) && hasValidCallbackFingerprint(value)\n );\n}\n\nfunction hasAttemptIdentity(value: Record<string, unknown>): boolean {\n return (\n value.version === ATTEMPT_VERSION &&\n typeof value.id === 'string' &&\n isSupabaseOAuthProviderId(typeof value.provider === 'string' ? value.provider : '') &&\n typeof value.redirectUri === 'string' &&\n (value.status === 'pending' || value.status === 'completing' || value.status === 'completed')\n );\n}\n\nfunction hasAttemptTimeline(value: Record<string, unknown>): boolean {\n return (\n typeof value.createdAt === 'number' &&\n Number.isSafeInteger(value.createdAt) &&\n value.createdAt >= 0 &&\n typeof value.expiresAt === 'number' &&\n Number.isSafeInteger(value.expiresAt) &&\n value.expiresAt > value.createdAt\n );\n}\n\nfunction hasValidCallbackFingerprint(value: Record<string, unknown>): boolean {\n if (value.callbackFingerprint === undefined) return true;\n return (\n (value.status === 'completing' || value.status === 'completed') &&\n typeof value.callbackFingerprint === 'string' &&\n /^[0-9a-f]{64}$/u.test(value.callbackFingerprint)\n );\n}\n\nfunction isStoredConsumedOAuthCallbacks(value: unknown): value is StoredConsumedOAuthCallbacks {\n return (\n isRecord(value) &&\n value.version === CONSUMED_CALLBACKS_VERSION &&\n Array.isArray(value.callbacks) &&\n value.callbacks.length <= MAX_CONSUMED_CALLBACKS &&\n value.callbacks.every(isStoredConsumedOAuthCallback)\n );\n}\n\nfunction isStoredConsumedOAuthCallback(value: unknown): value is StoredConsumedOAuthCallback {\n return (\n isRecord(value) &&\n typeof value.fingerprint === 'string' &&\n /^[0-9a-f]{64}$/u.test(value.fingerprint) &&\n typeof value.expiresAt === 'number' &&\n Number.isSafeInteger(value.expiresAt) &&\n value.expiresAt > 0\n );\n}\n\nasync function safeRemove(storage: SupabaseAuthStorage, key: string): Promise<void> {\n try {\n await storage.removeItem(key);\n } catch {\n // Cleanup failure is intentionally not exposed with storage contents or secret values.\n }\n}\n\nfunction digest(value: string): string {\n return bytesToHex(sha256(utf8ToBytes(value)));\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import type { AuthSession } from '@ankhorage/contracts/auth';
2
+ import type { SupabaseOAuthProviderId } from './oauthProviderDefinitions.js';
3
+ import type { SupabaseAuthFetch, SupabaseAuthRandomBytes, SupabaseAuthStorage, SupabaseOAuthLifecycleObserver, SupabaseOAuthProfileVerifier } from './types.js';
4
+ export declare const ATTEMPT_VERSION = 4;
5
+ export declare const DEFAULT_ATTEMPT_LIFETIME_MS: number;
6
+ export declare const CONSUMED_CALLBACKS_VERSION = 1;
7
+ export declare const CONSUMED_CALLBACK_RETENTION_MS: number;
8
+ export declare const MAX_CONSUMED_CALLBACKS = 32;
9
+ export interface CreateSupabaseOAuthAdapterInput {
10
+ url: string;
11
+ anonKey: string;
12
+ fetch: SupabaseAuthFetch;
13
+ storage: SupabaseAuthStorage;
14
+ storageKey: string;
15
+ providers: readonly SupabaseOAuthProviderId[];
16
+ randomBytes?: SupabaseAuthRandomBytes;
17
+ persistSession(session: AuthSession): Promise<void>;
18
+ verifyProfile?: SupabaseOAuthProfileVerifier;
19
+ onLifecycleEvent?: SupabaseOAuthLifecycleObserver;
20
+ now?: () => number;
21
+ attemptLifetimeMs?: number;
22
+ }
23
+ export interface StoredOAuthAttempt {
24
+ version: typeof ATTEMPT_VERSION;
25
+ id: string;
26
+ provider: SupabaseOAuthProviderId;
27
+ redirectUri: string;
28
+ status: 'pending' | 'completing' | 'completed';
29
+ createdAt: number;
30
+ expiresAt: number;
31
+ callbackFingerprint?: string;
32
+ }
33
+ export interface StoredConsumedOAuthCallback {
34
+ fingerprint: string;
35
+ expiresAt: number;
36
+ }
37
+ export interface StoredConsumedOAuthCallbacks {
38
+ version: typeof CONSUMED_CALLBACKS_VERSION;
39
+ callbacks: readonly StoredConsumedOAuthCallback[];
40
+ }
41
+ export type StoredOAuthAttemptReadResult = {
42
+ type: 'missing';
43
+ } | {
44
+ type: 'invalid';
45
+ } | {
46
+ type: 'valid';
47
+ attempt: StoredOAuthAttempt;
48
+ };
49
+ //# sourceMappingURL=oauthTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthTypes.d.ts","sourceRoot":"","sources":["../src/oauthTypes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,8BAA8B,EAC9B,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,2BAA2B,QAAiB,CAAC;AAC1D,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,eAAO,MAAM,8BAA8B,QAAsB,CAAC;AAClE,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,WAAW,+BAA+B;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,iBAAiB,CAAC;IACzB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC9C,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,aAAa,CAAC,EAAE,4BAA4B,CAAC;IAC7C,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;IAClD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,eAAe,CAAC;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,uBAAuB,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,CAAC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,OAAO,0BAA0B,CAAC;IAC3C,SAAS,EAAE,SAAS,2BAA2B,EAAE,CAAC;CACnD;AAED,MAAM,MAAM,4BAA4B,GACtC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAAC"}
@@ -0,0 +1,6 @@
1
+ export const ATTEMPT_VERSION = 4;
2
+ export const DEFAULT_ATTEMPT_LIFETIME_MS = 10 * 60 * 1000;
3
+ export const CONSUMED_CALLBACKS_VERSION = 1;
4
+ export const CONSUMED_CALLBACK_RETENTION_MS = 24 * 60 * 60 * 1000;
5
+ export const MAX_CONSUMED_CALLBACKS = 32;
6
+ //# sourceMappingURL=oauthTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthTypes.js","sourceRoot":"","sources":["../src/oauthTypes.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AACjC,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC1D,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAC5C,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAClE,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC","sourcesContent":["import type { AuthSession } from '@ankhorage/contracts/auth';\n\nimport type { SupabaseOAuthProviderId } from './oauthProviderDefinitions.js';\nimport type {\n SupabaseAuthFetch,\n SupabaseAuthRandomBytes,\n SupabaseAuthStorage,\n SupabaseOAuthLifecycleObserver,\n SupabaseOAuthProfileVerifier,\n} from './types.js';\n\nexport const ATTEMPT_VERSION = 4;\nexport const DEFAULT_ATTEMPT_LIFETIME_MS = 10 * 60 * 1000;\nexport const CONSUMED_CALLBACKS_VERSION = 1;\nexport const CONSUMED_CALLBACK_RETENTION_MS = 24 * 60 * 60 * 1000;\nexport const MAX_CONSUMED_CALLBACKS = 32;\n\nexport interface CreateSupabaseOAuthAdapterInput {\n url: string;\n anonKey: string;\n fetch: SupabaseAuthFetch;\n storage: SupabaseAuthStorage;\n storageKey: string;\n providers: readonly SupabaseOAuthProviderId[];\n randomBytes?: SupabaseAuthRandomBytes;\n persistSession(session: AuthSession): Promise<void>;\n verifyProfile?: SupabaseOAuthProfileVerifier;\n onLifecycleEvent?: SupabaseOAuthLifecycleObserver;\n now?: () => number;\n attemptLifetimeMs?: number;\n}\n\nexport interface StoredOAuthAttempt {\n version: typeof ATTEMPT_VERSION;\n id: string;\n provider: SupabaseOAuthProviderId;\n redirectUri: string;\n status: 'pending' | 'completing' | 'completed';\n createdAt: number;\n expiresAt: number;\n callbackFingerprint?: string;\n}\n\nexport interface StoredConsumedOAuthCallback {\n fingerprint: string;\n expiresAt: number;\n}\n\nexport interface StoredConsumedOAuthCallbacks {\n version: typeof CONSUMED_CALLBACKS_VERSION;\n callbacks: readonly StoredConsumedOAuthCallback[];\n}\n\nexport type StoredOAuthAttemptReadResult =\n { type: 'missing' } | { type: 'invalid' } | { type: 'valid'; attempt: StoredOAuthAttempt };\n"]}
@@ -0,0 +1,24 @@
1
+ import type { AuthSession } from '@ankhorage/contracts/auth';
2
+ import type { SupabaseAuthFetch, SupabaseAuthProfileField, SupabaseAuthProfileVerificationConfig, SupabaseOAuthProfileVerificationResult, SupabaseOAuthProfileVerifier } from './types.js';
3
+ interface NormalizedSupabaseAuthProfileVerificationConfig {
4
+ table: string;
5
+ fields: SupabaseAuthProfileField[];
6
+ maxAttempts: number;
7
+ retryDelayMs: number;
8
+ }
9
+ export declare function createSupabaseOAuthProfileVerifier(input: {
10
+ url: string;
11
+ anonKey: string;
12
+ fetch: SupabaseAuthFetch;
13
+ config: SupabaseAuthProfileVerificationConfig;
14
+ }): SupabaseOAuthProfileVerifier;
15
+ export declare function verifySupabaseOAuthProfile(input: {
16
+ url: string;
17
+ anonKey: string;
18
+ fetch: SupabaseAuthFetch;
19
+ config: SupabaseAuthProfileVerificationConfig;
20
+ session: AuthSession;
21
+ }): Promise<SupabaseOAuthProfileVerificationResult>;
22
+ export declare function normalizeSupabaseAuthProfileVerificationConfig(config: SupabaseAuthProfileVerificationConfig): NormalizedSupabaseAuthProfileVerificationConfig;
23
+ export {};
24
+ //# sourceMappingURL=profileVerification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileVerification.d.ts","sourceRoot":"","sources":["../src/profileVerification.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,KAAK,EACV,iBAAiB,EACjB,wBAAwB,EACxB,qCAAqC,EACrC,sCAAsC,EACtC,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AAiBpB,UAAU,+CAA+C;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,wBAAwB,EAAE,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,kCAAkC,CAAC,KAAK,EAAE;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,qCAAqC,CAAC;CAC/C,GAAG,4BAA4B,CAY/B;AAED,wBAAsB,0BAA0B,CAAC,KAAK,EAAE;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,qCAAqC,CAAC;IAC9C,OAAO,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,sCAAsC,CAAC,CAmBlD;AAqDD,wBAAgB,8CAA8C,CAC5D,MAAM,EAAE,qCAAqC,GAC5C,+CAA+C,CA8BjD"}
@@ -0,0 +1,178 @@
1
+ import { SUPABASE_AUTH_PROFILE_FIELDS } from './types.js';
2
+ const DEFAULT_PROFILE_FIELDS = ['email', 'displayName', 'avatarUrl'];
3
+ const DEFAULT_MAX_ATTEMPTS = 4;
4
+ const DEFAULT_RETRY_DELAY_MS = 75;
5
+ const PROFILE_COLUMN_BY_FIELD = {
6
+ email: 'email',
7
+ displayName: 'display_name',
8
+ avatarUrl: 'avatar_url',
9
+ username: 'username',
10
+ phone: 'phone',
11
+ };
12
+ const PROFILE_COLUMN_MAP = new Map(Object.entries(PROFILE_COLUMN_BY_FIELD));
13
+ export function createSupabaseOAuthProfileVerifier(input) {
14
+ const config = normalizeSupabaseAuthProfileVerificationConfig(input.config);
15
+ const url = input.url.replace(/\/+$/, '');
16
+ return async ({ session }) => verifySupabaseOAuthProfile({
17
+ url,
18
+ anonKey: input.anonKey,
19
+ fetch: input.fetch,
20
+ config,
21
+ session,
22
+ });
23
+ }
24
+ export async function verifySupabaseOAuthProfile(input) {
25
+ const config = normalizeSupabaseAuthProfileVerificationConfig(input.config);
26
+ const endpoint = createProfileEndpoint(input.url, config, input.session.user.id);
27
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
28
+ const result = await fetchProfileRows(input, endpoint);
29
+ if (result.type === 'failure')
30
+ return result.result;
31
+ if (result.type === 'rows' && result.rows.length > 0) {
32
+ return validateProfileRows(result.rows, input.session, config.fields);
33
+ }
34
+ if (attempt < config.maxAttempts) {
35
+ await delay(config.retryDelayMs);
36
+ }
37
+ else if (result.type === 'retry') {
38
+ return { ok: false, message: result.message };
39
+ }
40
+ }
41
+ return {
42
+ ok: false,
43
+ message: 'The auth identity exists but its generated public profile row was not found.',
44
+ };
45
+ }
46
+ async function fetchProfileRows(input, endpoint) {
47
+ let response;
48
+ try {
49
+ response = await input.fetch(endpoint, {
50
+ method: 'GET',
51
+ headers: {
52
+ Accept: 'application/json',
53
+ apikey: input.anonKey,
54
+ Authorization: `Bearer ${input.session.accessToken}`,
55
+ },
56
+ });
57
+ }
58
+ catch {
59
+ return {
60
+ type: 'retry',
61
+ message: 'Unable to reach the generated Supabase profile endpoint.',
62
+ };
63
+ }
64
+ if (!response.ok) {
65
+ return profileFailure(`Supabase profile verification failed with HTTP ${response.status}.`);
66
+ }
67
+ return parseProfileRows(response);
68
+ }
69
+ async function parseProfileRows(response) {
70
+ let body;
71
+ try {
72
+ body = await response.json();
73
+ }
74
+ catch {
75
+ return profileFailure('Supabase profile verification returned invalid JSON.');
76
+ }
77
+ return Array.isArray(body)
78
+ ? { type: 'rows', rows: body }
79
+ : profileFailure('Supabase profile verification returned an invalid row collection.');
80
+ }
81
+ function profileFailure(message) {
82
+ return { type: 'failure', result: { ok: false, message } };
83
+ }
84
+ export function normalizeSupabaseAuthProfileVerificationConfig(config) {
85
+ const table = config.table.trim();
86
+ if (table === 'users') {
87
+ throw new TypeError('Supabase profile verification must target an app-owned table such as "profiles", not public.users.');
88
+ }
89
+ if (!/^[a-z_][a-z0-9_]*$/u.test(table)) {
90
+ throw new TypeError('Supabase profile verification table must use snake_case letters, numbers, and underscores.');
91
+ }
92
+ const fields = uniqueProfileFields(config.fields ?? DEFAULT_PROFILE_FIELDS);
93
+ const maxAttempts = normalizeIntegerOption(config.maxAttempts, DEFAULT_MAX_ATTEMPTS, 1, 20, 'maxAttempts');
94
+ const retryDelayMs = normalizeIntegerOption(config.retryDelayMs, DEFAULT_RETRY_DELAY_MS, 0, 5_000, 'retryDelayMs');
95
+ return { table, fields, maxAttempts, retryDelayMs };
96
+ }
97
+ function createProfileEndpoint(baseUrl, config, userId) {
98
+ const url = new URL(`${baseUrl.replace(/\/+$/, '')}/rest/v1/${config.table}`);
99
+ const columns = ['id', ...config.fields.map((field) => PROFILE_COLUMN_MAP.get(field) ?? field)];
100
+ url.searchParams.set('id', `eq.${userId}`);
101
+ url.searchParams.set('select', [...new Set(columns)].join(','));
102
+ url.searchParams.set('limit', '2');
103
+ return url.toString();
104
+ }
105
+ function validateProfileRow(value, session, fields) {
106
+ if (!isRecord(value) || value.id !== session.user.id) {
107
+ return {
108
+ ok: false,
109
+ message: 'The generated public profile row does not match the authenticated identity.',
110
+ };
111
+ }
112
+ for (const field of fields) {
113
+ const expected = getExpectedProfileValue(session, field);
114
+ if (expected === undefined)
115
+ continue;
116
+ const column = PROFILE_COLUMN_MAP.get(field);
117
+ if (column === undefined || Reflect.get(value, column) !== expected) {
118
+ return {
119
+ ok: false,
120
+ message: `The generated public profile field "${field}" does not match provider metadata.`,
121
+ };
122
+ }
123
+ }
124
+ return { ok: true };
125
+ }
126
+ function validateProfileRows(rows, session, fields) {
127
+ if (rows.length !== 1) {
128
+ return {
129
+ ok: false,
130
+ message: 'The generated public profile lookup returned more than one row.',
131
+ };
132
+ }
133
+ return validateProfileRow(rows.at(0), session, fields);
134
+ }
135
+ function getExpectedProfileValue(session, field) {
136
+ switch (field) {
137
+ case 'email':
138
+ return session.user.email;
139
+ case 'displayName':
140
+ return session.user.displayName;
141
+ case 'avatarUrl':
142
+ return session.user.avatarUrl;
143
+ case 'username':
144
+ return session.user.username;
145
+ case 'phone':
146
+ return session.user.phone;
147
+ }
148
+ }
149
+ function uniqueProfileFields(fields) {
150
+ const supported = new Set(SUPABASE_AUTH_PROFILE_FIELDS);
151
+ const normalized = [];
152
+ for (const field of fields) {
153
+ if (!supported.has(field)) {
154
+ throw new TypeError(`Unsupported Supabase profile verification field "${field}".`);
155
+ }
156
+ if (!normalized.includes(field))
157
+ normalized.push(field);
158
+ }
159
+ return normalized;
160
+ }
161
+ function normalizeIntegerOption(value, fallback, minimum, maximum, name) {
162
+ const resolved = value ?? fallback;
163
+ if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) {
164
+ throw new TypeError(`Supabase profile verification ${name} must be an integer between ${minimum} and ${maximum}.`);
165
+ }
166
+ return resolved;
167
+ }
168
+ async function delay(milliseconds) {
169
+ if (milliseconds === 0)
170
+ return;
171
+ await new Promise((resolve) => {
172
+ setTimeout(resolve, milliseconds);
173
+ });
174
+ }
175
+ function isRecord(value) {
176
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
177
+ }
178
+ //# sourceMappingURL=profileVerification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileVerification.js","sourceRoot":"","sources":["../src/profileVerification.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,aAAa,EAAE,WAAW,CAAU,CAAC;AAC9E,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAC/B,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAClC,MAAM,uBAAuB,GAAG;IAC9B,KAAK,EAAE,OAAO;IACd,WAAW,EAAE,cAAc;IAC3B,SAAS,EAAE,YAAY;IACvB,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,OAAO;CAC6C,CAAC;AAC9D,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAChC,MAAM,CAAC,OAAO,CAAC,uBAAuB,CAAyC,CAChF,CAAC;AASF,MAAM,UAAU,kCAAkC,CAAC,KAKlD;IACC,MAAM,MAAM,GAAG,8CAA8C,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE1C,OAAO,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAC3B,0BAA0B,CAAC;QACzB,GAAG;QACH,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM;QACN,OAAO;KACR,CAAC,CAAC;AACP,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,KAMhD;IACC,MAAM,MAAM,GAAG,8CAA8C,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5E,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjF,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAClE,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACvD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC,MAAM,CAAC;QACpD,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrD,OAAO,mBAAmB,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,OAAO,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;YACjC,MAAM,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACnC,CAAC;aAAM,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QAChD,CAAC;IACH,CAAC;IACD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,8EAA8E;KACxF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,KAA0E,EAC1E,QAAgB;IAMhB,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE;YACrC,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACP,MAAM,EAAE,kBAAkB;gBAC1B,MAAM,EAAE,KAAK,CAAC,OAAO;gBACrB,aAAa,EAAE,UAAU,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE;aACrD;SACF,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,0DAA0D;SACpE,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,cAAc,CAAC,kDAAkD,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9F,CAAC;IACD,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,QAAkB;IAKlB,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,cAAc,CAAC,sDAAsD,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QACxB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE;QAC9B,CAAC,CAAC,cAAc,CAAC,mEAAmE,CAAC,CAAC;AAC1F,CAAC;AAED,SAAS,cAAc,CAAC,OAAe;IACrC,OAAO,EAAE,IAAI,EAAE,SAAkB,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAc,EAAE,OAAO,EAAE,EAAE,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,8CAA8C,CAC5D,MAA6C;IAE7C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACtB,MAAM,IAAI,SAAS,CACjB,oGAAoG,CACrG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,SAAS,CACjB,4FAA4F,CAC7F,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,mBAAmB,CAAC,MAAM,CAAC,MAAM,IAAI,sBAAsB,CAAC,CAAC;IAC5E,MAAM,WAAW,GAAG,sBAAsB,CACxC,MAAM,CAAC,WAAW,EAClB,oBAAoB,EACpB,CAAC,EACD,EAAE,EACF,aAAa,CACd,CAAC;IACF,MAAM,YAAY,GAAG,sBAAsB,CACzC,MAAM,CAAC,YAAY,EACnB,sBAAsB,EACtB,CAAC,EACD,KAAK,EACL,cAAc,CACf,CAAC;IAEF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AACtD,CAAC;AAED,SAAS,qBAAqB,CAC5B,OAAe,EACf,MAAuD,EACvD,MAAc;IAEd,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAC9E,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;IAChG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,MAAM,EAAE,CAAC,CAAC;IAC3C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,kBAAkB,CACzB,KAAc,EACd,OAAoB,EACpB,MAA2C;IAE3C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACrD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,6EAA6E;SACvF,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,uBAAuB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACzD,IAAI,QAAQ,KAAK,SAAS;YAAE,SAAS;QACrC,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC7C,IAAI,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;YACpE,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,uCAAuC,KAAK,qCAAqC;aAC3F,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,SAAS,mBAAmB,CAC1B,IAAwB,EACxB,OAAoB,EACpB,MAA2C;IAE3C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,iEAAiE;SAC3E,CAAC;IACJ,CAAC;IACD,OAAO,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,uBAAuB,CAC9B,OAAoB,EACpB,KAA+B;IAE/B,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,OAAO;YACV,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAC5B,KAAK,aAAa;YAChB,OAAO,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;QAClC,KAAK,WAAW;YACd,OAAO,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;QAChC,KAAK,UAAU;YACb,OAAO,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC/B,KAAK,OAAO;YACV,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAC1B,MAA2C;IAE3C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAS,4BAA4B,CAAC,CAAC;IAChE,MAAM,UAAU,GAA+B,EAAE,CAAC;IAElD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,SAAS,CAAC,oDAAoD,KAAK,IAAI,CAAC,CAAC;QACrF,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,sBAAsB,CAC7B,KAAyB,EACzB,QAAgB,EAChB,OAAe,EACf,OAAe,EACf,IAAY;IAEZ,MAAM,QAAQ,GAAG,KAAK,IAAI,QAAQ,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,OAAO,IAAI,QAAQ,GAAG,OAAO,EAAE,CAAC;QAC5E,MAAM,IAAI,SAAS,CACjB,iCAAiC,IAAI,+BAA+B,OAAO,QAAQ,OAAO,GAAG,CAC9F,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,KAAK,CAAC,YAAoB;IACvC,IAAI,YAAY,KAAK,CAAC;QAAE,OAAO;IAC/B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["import type { AuthSession } from '@ankhorage/contracts/auth';\n\nimport type {\n SupabaseAuthFetch,\n SupabaseAuthProfileField,\n SupabaseAuthProfileVerificationConfig,\n SupabaseOAuthProfileVerificationResult,\n SupabaseOAuthProfileVerifier,\n} from './types.js';\nimport { SUPABASE_AUTH_PROFILE_FIELDS } from './types.js';\n\nconst DEFAULT_PROFILE_FIELDS = ['email', 'displayName', 'avatarUrl'] as const;\nconst DEFAULT_MAX_ATTEMPTS = 4;\nconst DEFAULT_RETRY_DELAY_MS = 75;\nconst PROFILE_COLUMN_BY_FIELD = {\n email: 'email',\n displayName: 'display_name',\n avatarUrl: 'avatar_url',\n username: 'username',\n phone: 'phone',\n} as const satisfies Record<SupabaseAuthProfileField, string>;\nconst PROFILE_COLUMN_MAP = new Map<SupabaseAuthProfileField, string>(\n Object.entries(PROFILE_COLUMN_BY_FIELD) as [SupabaseAuthProfileField, string][],\n);\n\ninterface NormalizedSupabaseAuthProfileVerificationConfig {\n table: string;\n fields: SupabaseAuthProfileField[];\n maxAttempts: number;\n retryDelayMs: number;\n}\n\nexport function createSupabaseOAuthProfileVerifier(input: {\n url: string;\n anonKey: string;\n fetch: SupabaseAuthFetch;\n config: SupabaseAuthProfileVerificationConfig;\n}): SupabaseOAuthProfileVerifier {\n const config = normalizeSupabaseAuthProfileVerificationConfig(input.config);\n const url = input.url.replace(/\\/+$/, '');\n\n return async ({ session }) =>\n verifySupabaseOAuthProfile({\n url,\n anonKey: input.anonKey,\n fetch: input.fetch,\n config,\n session,\n });\n}\n\nexport async function verifySupabaseOAuthProfile(input: {\n url: string;\n anonKey: string;\n fetch: SupabaseAuthFetch;\n config: SupabaseAuthProfileVerificationConfig;\n session: AuthSession;\n}): Promise<SupabaseOAuthProfileVerificationResult> {\n const config = normalizeSupabaseAuthProfileVerificationConfig(input.config);\n const endpoint = createProfileEndpoint(input.url, config, input.session.user.id);\n for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {\n const result = await fetchProfileRows(input, endpoint);\n if (result.type === 'failure') return result.result;\n if (result.type === 'rows' && result.rows.length > 0) {\n return validateProfileRows(result.rows, input.session, config.fields);\n }\n if (attempt < config.maxAttempts) {\n await delay(config.retryDelayMs);\n } else if (result.type === 'retry') {\n return { ok: false, message: result.message };\n }\n }\n return {\n ok: false,\n message: 'The auth identity exists but its generated public profile row was not found.',\n };\n}\n\nasync function fetchProfileRows(\n input: { anonKey: string; fetch: SupabaseAuthFetch; session: AuthSession },\n endpoint: string,\n): Promise<\n | { type: 'retry'; message: string }\n | { type: 'rows'; rows: unknown[] }\n | { type: 'failure'; result: SupabaseOAuthProfileVerificationResult }\n> {\n let response: Response;\n try {\n response = await input.fetch(endpoint, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n apikey: input.anonKey,\n Authorization: `Bearer ${input.session.accessToken}`,\n },\n });\n } catch {\n return {\n type: 'retry',\n message: 'Unable to reach the generated Supabase profile endpoint.',\n };\n }\n if (!response.ok) {\n return profileFailure(`Supabase profile verification failed with HTTP ${response.status}.`);\n }\n return parseProfileRows(response);\n}\n\nasync function parseProfileRows(\n response: Response,\n): Promise<\n | { type: 'rows'; rows: unknown[] }\n | { type: 'failure'; result: SupabaseOAuthProfileVerificationResult }\n> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n return profileFailure('Supabase profile verification returned invalid JSON.');\n }\n return Array.isArray(body)\n ? { type: 'rows', rows: body }\n : profileFailure('Supabase profile verification returned an invalid row collection.');\n}\n\nfunction profileFailure(message: string) {\n return { type: 'failure' as const, result: { ok: false as const, message } };\n}\n\nexport function normalizeSupabaseAuthProfileVerificationConfig(\n config: SupabaseAuthProfileVerificationConfig,\n): NormalizedSupabaseAuthProfileVerificationConfig {\n const table = config.table.trim();\n if (table === 'users') {\n throw new TypeError(\n 'Supabase profile verification must target an app-owned table such as \"profiles\", not public.users.',\n );\n }\n if (!/^[a-z_][a-z0-9_]*$/u.test(table)) {\n throw new TypeError(\n 'Supabase profile verification table must use snake_case letters, numbers, and underscores.',\n );\n }\n\n const fields = uniqueProfileFields(config.fields ?? DEFAULT_PROFILE_FIELDS);\n const maxAttempts = normalizeIntegerOption(\n config.maxAttempts,\n DEFAULT_MAX_ATTEMPTS,\n 1,\n 20,\n 'maxAttempts',\n );\n const retryDelayMs = normalizeIntegerOption(\n config.retryDelayMs,\n DEFAULT_RETRY_DELAY_MS,\n 0,\n 5_000,\n 'retryDelayMs',\n );\n\n return { table, fields, maxAttempts, retryDelayMs };\n}\n\nfunction createProfileEndpoint(\n baseUrl: string,\n config: NormalizedSupabaseAuthProfileVerificationConfig,\n userId: string,\n): string {\n const url = new URL(`${baseUrl.replace(/\\/+$/, '')}/rest/v1/${config.table}`);\n const columns = ['id', ...config.fields.map((field) => PROFILE_COLUMN_MAP.get(field) ?? field)];\n url.searchParams.set('id', `eq.${userId}`);\n url.searchParams.set('select', [...new Set(columns)].join(','));\n url.searchParams.set('limit', '2');\n return url.toString();\n}\n\nfunction validateProfileRow(\n value: unknown,\n session: AuthSession,\n fields: readonly SupabaseAuthProfileField[],\n): SupabaseOAuthProfileVerificationResult {\n if (!isRecord(value) || value.id !== session.user.id) {\n return {\n ok: false,\n message: 'The generated public profile row does not match the authenticated identity.',\n };\n }\n\n for (const field of fields) {\n const expected = getExpectedProfileValue(session, field);\n if (expected === undefined) continue;\n const column = PROFILE_COLUMN_MAP.get(field);\n if (column === undefined || Reflect.get(value, column) !== expected) {\n return {\n ok: false,\n message: `The generated public profile field \"${field}\" does not match provider metadata.`,\n };\n }\n }\n\n return { ok: true };\n}\n\nfunction validateProfileRows(\n rows: readonly unknown[],\n session: AuthSession,\n fields: readonly SupabaseAuthProfileField[],\n): SupabaseOAuthProfileVerificationResult {\n if (rows.length !== 1) {\n return {\n ok: false,\n message: 'The generated public profile lookup returned more than one row.',\n };\n }\n return validateProfileRow(rows.at(0), session, fields);\n}\n\nfunction getExpectedProfileValue(\n session: AuthSession,\n field: SupabaseAuthProfileField,\n): string | undefined {\n switch (field) {\n case 'email':\n return session.user.email;\n case 'displayName':\n return session.user.displayName;\n case 'avatarUrl':\n return session.user.avatarUrl;\n case 'username':\n return session.user.username;\n case 'phone':\n return session.user.phone;\n }\n}\n\nfunction uniqueProfileFields(\n fields: readonly SupabaseAuthProfileField[],\n): SupabaseAuthProfileField[] {\n const supported = new Set<string>(SUPABASE_AUTH_PROFILE_FIELDS);\n const normalized: SupabaseAuthProfileField[] = [];\n\n for (const field of fields) {\n if (!supported.has(field)) {\n throw new TypeError(`Unsupported Supabase profile verification field \"${field}\".`);\n }\n if (!normalized.includes(field)) normalized.push(field);\n }\n\n return normalized;\n}\n\nfunction normalizeIntegerOption(\n value: number | undefined,\n fallback: number,\n minimum: number,\n maximum: number,\n name: string,\n): number {\n const resolved = value ?? fallback;\n if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) {\n throw new TypeError(\n `Supabase profile verification ${name} must be an integer between ${minimum} and ${maximum}.`,\n );\n }\n return resolved;\n}\n\nasync function delay(milliseconds: number): Promise<void> {\n if (milliseconds === 0) return;\n await new Promise<void>((resolve) => {\n setTimeout(resolve, milliseconds);\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { AuthSession, AuthUser } from '@ankhorage/contracts/auth';
2
+ export declare function normalizeSupabaseSession(body: unknown, now?: number): AuthSession | null;
3
+ export declare function normalizeSupabaseUser(body: unknown): AuthUser | null;
4
+ export declare function parseStoredSession(value: string | null): AuthSession | null;
5
+ export declare function isAuthSessionExpired(session: AuthSession | null | undefined, now?: number): boolean;
6
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAEvE,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,SAAa,GAAG,WAAW,GAAG,IAAI,CAa5F;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CA8BpE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,GAAG,IAAI,CAc3E;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACvC,GAAG,SAAa,GACf,OAAO,CAGT"}