@yielded/auth-persistence 0.1.0-beta.22 → 0.1.0-beta.24

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 (182) hide show
  1. package/README.md +11 -0
  2. package/dist/Adapter.d.mts +13 -5
  3. package/dist/Adapter.mjs +9 -2
  4. package/dist/OAuthPersistence.d.mts +87 -0
  5. package/dist/OAuthPersistence.mjs +125 -0
  6. package/dist/OAuthPersistence.mjs.map +1 -0
  7. package/dist/_virtual/_rolldown/runtime.mjs +13 -0
  8. package/dist/index.d.mts +3 -1
  9. package/dist/index.mjs +3 -1
  10. package/dist/internal/authority-snapshot.mjs +53 -0
  11. package/dist/internal/authority-snapshot.mjs.map +1 -0
  12. package/dist/internal/configuration.d.mts +1 -1
  13. package/dist/internal/configuration.mjs.map +1 -1
  14. package/dist/internal/models/oauth-connected-model.d.mts +294 -0
  15. package/dist/internal/models/oauth-connected-model.mjs +22 -0
  16. package/dist/internal/models/oauth-connected-model.mjs.map +1 -0
  17. package/dist/internal/models/oauth-model.d.mts +467 -0
  18. package/dist/internal/models/oauth-model.mjs +49 -0
  19. package/dist/internal/models/oauth-model.mjs.map +1 -0
  20. package/dist/internal/models/password-model.d.mts +4 -97
  21. package/dist/internal/models/password-model.mjs +0 -3
  22. package/dist/internal/models/password-model.mjs.map +1 -1
  23. package/dist/internal/native-sql-table.d.mts +34 -0
  24. package/dist/internal/native-sql-table.mjs +111 -0
  25. package/dist/internal/native-sql-table.mjs.map +1 -0
  26. package/dist/internal/oauth/accounts.d.mts +8 -0
  27. package/dist/internal/oauth/accounts.mjs +379 -0
  28. package/dist/internal/oauth/accounts.mjs.map +1 -0
  29. package/dist/internal/oauth/connected-access.d.mts +9 -0
  30. package/dist/internal/oauth/connected-access.mjs +229 -0
  31. package/dist/internal/oauth/connected-access.mjs.map +1 -0
  32. package/dist/internal/oauth/connected-collection.d.mts +7 -0
  33. package/dist/internal/oauth/connected-collection.mjs +214 -0
  34. package/dist/internal/oauth/connected-collection.mjs.map +1 -0
  35. package/dist/internal/oauth/connected-custody.d.mts +6 -0
  36. package/dist/internal/oauth/connected-custody.mjs +165 -0
  37. package/dist/internal/oauth/connected-custody.mjs.map +1 -0
  38. package/dist/internal/oauth/connected-flow.d.mts +7 -0
  39. package/dist/internal/oauth/connected-flow.mjs +324 -0
  40. package/dist/internal/oauth/connected-flow.mjs.map +1 -0
  41. package/dist/internal/oauth/connected-input.mjs +122 -0
  42. package/dist/internal/oauth/connected-input.mjs.map +1 -0
  43. package/dist/internal/oauth/connected-keys.d.mts +2 -0
  44. package/dist/internal/oauth/connected-keys.mjs +22 -0
  45. package/dist/internal/oauth/connected-keys.mjs.map +1 -0
  46. package/dist/internal/oauth/connected-maintenance.d.mts +9 -0
  47. package/dist/internal/oauth/connected-maintenance.mjs +226 -0
  48. package/dist/internal/oauth/connected-maintenance.mjs.map +1 -0
  49. package/dist/internal/oauth/connected-management.d.mts +9 -0
  50. package/dist/internal/oauth/connected-management.mjs +243 -0
  51. package/dist/internal/oauth/connected-management.mjs.map +1 -0
  52. package/dist/internal/oauth/connected-reference.mjs +63 -0
  53. package/dist/internal/oauth/connected-reference.mjs.map +1 -0
  54. package/dist/internal/oauth/connected-settlement.d.mts +8 -0
  55. package/dist/internal/oauth/connected-settlement.mjs +141 -0
  56. package/dist/internal/oauth/connected-settlement.mjs.map +1 -0
  57. package/dist/internal/oauth/connected-sign-in.d.mts +10 -0
  58. package/dist/internal/oauth/connected-sign-in.mjs +281 -0
  59. package/dist/internal/oauth/connected-sign-in.mjs.map +1 -0
  60. package/dist/internal/oauth/connected-state.d.mts +14 -0
  61. package/dist/internal/oauth/connected-state.mjs +469 -0
  62. package/dist/internal/oauth/connected-state.mjs.map +1 -0
  63. package/dist/internal/oauth/connected-target.d.mts +21 -0
  64. package/dist/internal/oauth/connected-target.mjs +117 -0
  65. package/dist/internal/oauth/connected-target.mjs.map +1 -0
  66. package/dist/internal/oauth/flow.d.mts +5 -0
  67. package/dist/internal/oauth/flow.mjs +279 -0
  68. package/dist/internal/oauth/flow.mjs.map +1 -0
  69. package/dist/internal/oauth/input.mjs +127 -0
  70. package/dist/internal/oauth/input.mjs.map +1 -0
  71. package/dist/internal/oauth/owner.d.mts +10 -0
  72. package/dist/internal/oauth/owner.mjs +10 -0
  73. package/dist/internal/oauth/owner.mjs.map +1 -0
  74. package/dist/internal/oauth/query-compiler.d.mts +7 -0
  75. package/dist/internal/oauth/query-compiler.mjs +9 -0
  76. package/dist/internal/oauth/query-compiler.mjs.map +1 -0
  77. package/dist/internal/oauth/registration.d.mts +7 -0
  78. package/dist/internal/oauth/registration.mjs +393 -0
  79. package/dist/internal/oauth/registration.mjs.map +1 -0
  80. package/dist/internal/oauth/state.mjs +107 -0
  81. package/dist/internal/oauth/state.mjs.map +1 -0
  82. package/dist/internal/oauth/target.d.mts +31 -0
  83. package/dist/internal/oauth/target.mjs +273 -0
  84. package/dist/internal/oauth/target.mjs.map +1 -0
  85. package/dist/internal/oauth-kernel.d.mts +1423 -0
  86. package/dist/internal/oauth-kernel.mjs +187 -0
  87. package/dist/internal/oauth-kernel.mjs.map +1 -0
  88. package/dist/internal/oauth-proxy.d.mts +31 -0
  89. package/dist/internal/oauth-proxy.mjs +129 -0
  90. package/dist/internal/oauth-proxy.mjs.map +1 -0
  91. package/dist/internal/passkey/target.d.mts +1 -1
  92. package/dist/internal/passkey-kernel.d.mts +5 -5
  93. package/dist/internal/passkeys.mjs +1 -1
  94. package/dist/internal/password-credentials.d.mts +11 -0
  95. package/dist/internal/password-credentials.mjs +121 -0
  96. package/dist/internal/password-credentials.mjs.map +1 -0
  97. package/dist/internal/password-kernel.d.mts +15 -28
  98. package/dist/internal/password-kernel.mjs +35 -236
  99. package/dist/internal/password-kernel.mjs.map +1 -1
  100. package/dist/internal/persistence.d.mts +2 -0
  101. package/dist/internal/persistence.mjs +5 -5
  102. package/dist/internal/persistence.mjs.map +1 -1
  103. package/dist/internal/query-operations.d.mts +2 -0
  104. package/dist/internal/session-kernel.d.mts +5 -3
  105. package/dist/internal/session-kernel.mjs +95 -29
  106. package/dist/internal/session-kernel.mjs.map +1 -1
  107. package/dist/internal/sql-oauth-connected-model.d.mts +29 -0
  108. package/dist/internal/sql-oauth-connected-model.mjs +7 -0
  109. package/dist/internal/sql-oauth-connected-model.mjs.map +1 -0
  110. package/dist/internal/sql-oauth-model.d.mts +60 -0
  111. package/dist/internal/sql-oauth-model.mjs +10 -0
  112. package/dist/internal/sql-oauth-model.mjs.map +1 -0
  113. package/dist/internal/sql-persistence.mjs +3 -1
  114. package/dist/internal/sql-persistence.mjs.map +1 -1
  115. package/dist/internal/sql-query.d.mts +12 -0
  116. package/dist/internal/sql-query.mjs +15 -18
  117. package/dist/internal/sql-query.mjs.map +1 -1
  118. package/dist/internal/sql-storage-validation.mjs +54 -0
  119. package/dist/internal/sql-storage-validation.mjs.map +1 -0
  120. package/dist/internal/storage-mapping.mjs +2 -67
  121. package/dist/internal/storage-mapping.mjs.map +1 -1
  122. package/dist/internal/storage-plans.d.mts +7 -0
  123. package/dist/internal/storage-plans.mjs +189 -0
  124. package/dist/internal/storage-plans.mjs.map +1 -0
  125. package/dist/internal/storage-tables.d.mts +0 -99
  126. package/dist/internal/storage-tables.mjs +0 -43
  127. package/dist/internal/storage-tables.mjs.map +1 -1
  128. package/dist/internal/storage-validation.d.mts +6 -1
  129. package/dist/internal/storage-validation.mjs +42 -1
  130. package/dist/internal/storage-validation.mjs.map +1 -1
  131. package/dist/internal/transaction-execution-kernel.d.mts +1 -1
  132. package/dist/internal/transaction-kernel.d.mts +1 -1
  133. package/dist/internal/transaction-kernel.mjs +3 -2
  134. package/dist/internal/transaction-kernel.mjs.map +1 -1
  135. package/package.json +1 -1
  136. package/src/Adapter.ts +97 -8
  137. package/src/OAuthPersistence.ts +427 -0
  138. package/src/index.ts +3 -0
  139. package/src/internal/authority-snapshot.ts +91 -0
  140. package/src/internal/configuration.ts +1 -8
  141. package/src/internal/models/oauth-connected-model.ts +396 -0
  142. package/src/internal/models/oauth-model.ts +708 -0
  143. package/src/internal/models/password-model.ts +0 -120
  144. package/src/internal/native-sql-table.ts +178 -0
  145. package/src/internal/oauth/accounts.ts +838 -0
  146. package/src/internal/oauth/connected-access.ts +388 -0
  147. package/src/internal/oauth/connected-collection.ts +345 -0
  148. package/src/internal/oauth/connected-custody.ts +308 -0
  149. package/src/internal/oauth/connected-flow.ts +562 -0
  150. package/src/internal/oauth/connected-input.ts +147 -0
  151. package/src/internal/oauth/connected-keys.ts +31 -0
  152. package/src/internal/oauth/connected-maintenance.ts +379 -0
  153. package/src/internal/oauth/connected-management.ts +391 -0
  154. package/src/internal/oauth/connected-reference.ts +135 -0
  155. package/src/internal/oauth/connected-settlement.ts +278 -0
  156. package/src/internal/oauth/connected-sign-in.ts +472 -0
  157. package/src/internal/oauth/connected-state.ts +924 -0
  158. package/src/internal/oauth/connected-target.ts +307 -0
  159. package/src/internal/oauth/flow.ts +505 -0
  160. package/src/internal/oauth/input.ts +201 -0
  161. package/src/internal/oauth/owner.ts +22 -0
  162. package/src/internal/oauth/query-compiler.ts +15 -0
  163. package/src/internal/oauth/registration.ts +676 -0
  164. package/src/internal/oauth/state.ts +218 -0
  165. package/src/internal/oauth/target.ts +803 -0
  166. package/src/internal/oauth-kernel.ts +294 -0
  167. package/src/internal/oauth-proxy.ts +176 -0
  168. package/src/internal/password-credentials.ts +245 -0
  169. package/src/internal/password-kernel.ts +98 -712
  170. package/src/internal/persistence.ts +5 -10
  171. package/src/internal/query-operations.ts +2 -0
  172. package/src/internal/session-kernel.ts +231 -81
  173. package/src/internal/sql-oauth-connected-model.ts +145 -0
  174. package/src/internal/sql-oauth-model.ts +263 -0
  175. package/src/internal/sql-persistence.ts +2 -0
  176. package/src/internal/sql-query.ts +16 -13
  177. package/src/internal/sql-storage-validation.ts +64 -0
  178. package/src/internal/storage-mapping.ts +0 -66
  179. package/src/internal/storage-plans.ts +218 -0
  180. package/src/internal/storage-tables.ts +0 -35
  181. package/src/internal/storage-validation.ts +58 -0
  182. package/src/internal/transaction-kernel.ts +6 -2
@@ -0,0 +1,245 @@
1
+ import { CurrentCommitJournal, hasCommitScope } from "@yielded/auth/Hooks";
2
+ import type { LoginIdentifier } from "@yielded/auth/Identity";
3
+ import {
4
+ EncodedPasswordHash,
5
+ type PasswordPersistence,
6
+ PasswordUnavailable,
7
+ snapshotPasswordCredential,
8
+ type PasswordCredentialSnapshot,
9
+ } from "@yielded/auth/Password";
10
+ import { reportPersistenceFailure } from "@yielded/auth/Persistence";
11
+ import { Effect, Option, Redacted, Schema } from "effect";
12
+
13
+ import type { PersistenceMappingError } from "./mapping-error";
14
+ import type { AnyPasswordPersistenceMapping } from "./models/password-model";
15
+ import { sqlMapping, type SqlTable } from "./native-sql-table";
16
+ import type { PasswordSqlConfiguration } from "./password-kernel";
17
+ import { NativeDatabase } from "./transaction-kernel";
18
+
19
+ type Row = Readonly<Record<string, unknown>>;
20
+ const unavailable = () => PasswordUnavailable.make({});
21
+
22
+ const failure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
23
+ reportPersistenceFailure(effect, Schema.is(PasswordUnavailable)).pipe(
24
+ Effect.mapError(unavailable),
25
+ );
26
+
27
+ export const samePasswordCredentialSnapshot = (
28
+ a: PasswordCredentialSnapshot,
29
+ b: PasswordCredentialSnapshot,
30
+ ) =>
31
+ a.moduleId === b.moduleId &&
32
+ a.revision.subjectId === b.revision.subjectId &&
33
+ a.revision.securityRevision === b.revision.securityRevision &&
34
+ a.revision.credentials.length === b.revision.credentials.length &&
35
+ new Set(a.revision.credentials.map((entry) => entry.credentialId)).size ===
36
+ a.revision.credentials.length &&
37
+ new Set(b.revision.credentials.map((entry) => entry.credentialId)).size ===
38
+ b.revision.credentials.length &&
39
+ a.revision.credentials.every((entry) =>
40
+ b.revision.credentials.some(
41
+ (other) => entry.credentialId === other.credentialId && entry.revision === other.revision,
42
+ ),
43
+ ) &&
44
+ a.credentialId === b.credentialId &&
45
+ a.credentialRevision === b.credentialRevision &&
46
+ a.verifierVersion === b.verifierVersion &&
47
+ Redacted.value(a.verifier) === Redacted.value(b.verifier) &&
48
+ a.normalization === b.normalization &&
49
+ a.identifier.namespace === b.identifier.namespace &&
50
+ a.identifier.value === b.identifier.value &&
51
+ a.identifierBindingRevision === b.identifierBindingRevision &&
52
+ a.identifierVerifiedAtMillis === b.identifierVerifiedAtMillis;
53
+
54
+ /** Lookup is read-only; rehash is one conditional maintenance write, never authentication. */
55
+ export const makePasswordCredentials = Effect.fnUntraced(function* (
56
+ table: (table: object) => SqlTable,
57
+ mapping: AnyPasswordPersistenceMapping,
58
+ options: PasswordSqlConfiguration,
59
+ ): Effect.fn.Return<
60
+ Pick<PasswordPersistence["Service"], "findCredential" | "rehashIfCurrent">,
61
+ PasswordUnavailable,
62
+ NativeDatabase
63
+ > {
64
+ const sql = (yield* NativeDatabase).$client.withoutTransforms();
65
+ const parent = yield* Effect.serviceOption(CurrentCommitJournal);
66
+
67
+ const [s, i, c] = yield* sqlMapping(
68
+ () =>
69
+ [
70
+ table(mapping.subject.table),
71
+ table(mapping.identifier.table),
72
+ table(mapping.credential.table),
73
+ ] as const,
74
+ ).pipe(failure);
75
+
76
+ const standalone = Effect.gen(function* () {
77
+ if (options.coordinated) {
78
+ const current = yield* Effect.serviceOption(CurrentCommitJournal);
79
+
80
+ if (Option.isNone(parent) || Option.isNone(current) || parent.value !== current.value)
81
+ return yield* unavailable();
82
+ } else {
83
+ if (yield* hasCommitScope) return yield* unavailable();
84
+ yield* options.standaloneGuard;
85
+ }
86
+ });
87
+
88
+ const allocate = <A>(
89
+ asynchronous: Effect.Effect<A, PersistenceMappingError> | undefined,
90
+ sync: (() => A) | undefined,
91
+ ) => {
92
+ if (options.mode !== "synchronous" && asynchronous !== undefined) return asynchronous;
93
+
94
+ return sync === undefined
95
+ ? Effect.fail(unavailable())
96
+ : Effect.try({ try: sync, catch: unavailable });
97
+ };
98
+
99
+ // Bind each native ID through its own column codec. Physical encodings of the
100
+ // same application ID need not match across subject, identifier and password.
101
+ const snapshot = Effect.fnUntraced(function* (
102
+ nativeId: unknown,
103
+ moduleId: string,
104
+ identifier: LoginIdentifier,
105
+ ) {
106
+ const subjectTable = s.as("password_subject");
107
+ const identifierTable = i.as("password_identifier");
108
+ const credentialTable = c.as("password_credential");
109
+
110
+ const rows = yield* sqlMapping(
111
+ () =>
112
+ sql<Row>`select ${subjectTable.fields("subject_")}, ${identifierTable.fields("identifier_")}, ${credentialTable.fields("credential_")}
113
+ from ${subjectTable.name}
114
+ inner join ${identifierTable.name} on ${identifierTable.column(mapping.identifier.namespace)} = ${identifierTable.value(mapping.identifier.namespace, identifier.namespace)}
115
+ and ${identifierTable.column(mapping.identifier.value)} = ${identifierTable.value(mapping.identifier.value, identifier.value)}
116
+ and ${identifierTable.column(mapping.identifier.subjectId)} = ${identifierTable.value(mapping.identifier.subjectId, nativeId)}
117
+ inner join ${credentialTable.name} on ${credentialTable.column(mapping.credential.moduleId)} = ${credentialTable.value(mapping.credential.moduleId, moduleId)}
118
+ and ${credentialTable.column(mapping.credential.subjectId)} = ${credentialTable.value(mapping.credential.subjectId, nativeId)}
119
+ where ${subjectTable.column(mapping.subject.id)} = ${subjectTable.value(mapping.subject.id, nativeId)}
120
+ `,
121
+ ).pipe(Effect.flatten);
122
+
123
+ if (rows.length !== 1) return undefined;
124
+
125
+ const [subject, identifierRow, credential] = yield* sqlMapping(
126
+ () =>
127
+ [
128
+ s.decode(rows[0]!, "subject_"),
129
+ i.decode(rows[0]!, "identifier_"),
130
+ c.decode(rows[0]!, "credential_"),
131
+ ] as const,
132
+ );
133
+
134
+ if (
135
+ yield* sqlMapping(
136
+ () =>
137
+ !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
138
+ !mapping.identifier.isCurrent(identifierRow) ||
139
+ !mapping.subjectId.equals(nativeId, identifierRow[mapping.identifier.subjectId]) ||
140
+ !mapping.subjectId.equals(nativeId, subject[mapping.subject.id]) ||
141
+ !mapping.subjectId.equals(nativeId, credential[mapping.credential.subjectId]),
142
+ )
143
+ )
144
+ return undefined;
145
+
146
+ const captured = yield* sqlMapping(() =>
147
+ mapping.credential.decode({ moduleId, subject, identifier: identifierRow, credential }),
148
+ ).pipe(Effect.flatten, Effect.flatMap(snapshotPasswordCredential));
149
+
150
+ return captured;
151
+ });
152
+
153
+ return {
154
+ findCredential: (uncaptured) =>
155
+ Effect.gen(function* () {
156
+ const input = { ...uncaptured, identifier: { ...uncaptured.identifier } };
157
+
158
+ yield* standalone;
159
+
160
+ const identifiers = yield* sqlMapping(
161
+ () => sql<Row>`select ${i.fields("i_")} from ${i.name}
162
+ where ${i.column(mapping.identifier.namespace)} = ${i.value(mapping.identifier.namespace, input.identifier.namespace)}
163
+ and ${i.column(mapping.identifier.value)} = ${i.value(mapping.identifier.value, input.identifier.value)}`,
164
+ ).pipe(Effect.flatten);
165
+
166
+ const identifier =
167
+ identifiers.length === 1
168
+ ? yield* sqlMapping(() => i.decode(identifiers[0]!, "i_"))
169
+ : undefined;
170
+
171
+ if (
172
+ identifier === undefined ||
173
+ !(yield* sqlMapping(() => mapping.identifier.isCurrent(identifier)))
174
+ )
175
+ return Option.none();
176
+
177
+ const credential = yield* snapshot(
178
+ identifier[mapping.identifier.subjectId],
179
+ input.moduleId,
180
+ input.identifier,
181
+ );
182
+
183
+ return credential === undefined ||
184
+ (input.subjectId !== undefined && credential.revision.subjectId !== input.subjectId)
185
+ ? Option.none()
186
+ : Option.some(credential);
187
+ }).pipe(failure),
188
+ rehashIfCurrent: (input) =>
189
+ Effect.gen(function* () {
190
+ const nextVerifier = Redacted.make(
191
+ yield* Schema.decodeEffect(EncodedPasswordHash)(Redacted.value(input.nextVerifier)),
192
+ );
193
+
194
+ const credential = yield* snapshotPasswordCredential(input.credential);
195
+
196
+ yield* standalone;
197
+ const nativeId = yield* mapping.subjectId.toNative(credential.revision.subjectId);
198
+ const version = yield* allocate(mapping.allocateRevision, mapping.allocateRevisionSync);
199
+
200
+ if (version === credential.verifierVersion) return yield* unavailable();
201
+ yield* sqlMapping(() => {
202
+ const encoded = mapping.credential.encodeVerifier(nextVerifier, version);
203
+
204
+ // A custom encoder cannot turn maintenance into a semantic credential change.
205
+ const values = {
206
+ [mapping.credential.verifier]: encoded[mapping.credential.verifier],
207
+ [mapping.credential.verifierVersion]: encoded[mapping.credential.verifierVersion],
208
+ };
209
+
210
+ if (Object.values(values).some((value) => value === undefined)) throw unavailable();
211
+
212
+ // Ordinary identity predicates retain index use; the full CAS ignores text collation.
213
+ const exact = sql.and(
214
+ Object.entries({
215
+ [mapping.credential.moduleId]: credential.moduleId,
216
+ [mapping.credential.subjectId]: nativeId,
217
+ [mapping.credential.credentialId]: credential.credentialId,
218
+ [mapping.credential.credentialRevision]: credential.credentialRevision,
219
+ [mapping.credential.verifierVersion]: credential.verifierVersion,
220
+ [mapping.credential.verifier]: Redacted.value(credential.verifier),
221
+ [mapping.credential.normalization]: credential.normalization,
222
+ }).map(([key, value]) => {
223
+ const column = c.column(key),
224
+ bound = c.value(key, value);
225
+
226
+ if (typeof value !== "string") return sql`${column} = ${bound}`;
227
+
228
+ return sql.onDialectOrElse({
229
+ mysql: () => sql`binary ${column} = binary ${bound}`,
230
+ pg: () =>
231
+ sql`convert_to(cast(${column} as text), 'UTF8') = convert_to(cast(${bound} as text), 'UTF8')`,
232
+ orElse: () => sql`cast(${column} as blob) = cast(${bound} as blob)`,
233
+ });
234
+ }),
235
+ );
236
+
237
+ return sql`${c.update(values)}
238
+ where ${c.column(mapping.credential.moduleId)} = ${c.value(mapping.credential.moduleId, credential.moduleId)}
239
+ and ${c.column(mapping.credential.subjectId)} = ${c.value(mapping.credential.subjectId, nativeId)}
240
+ and ${c.column(mapping.credential.credentialId)} = ${c.value(mapping.credential.credentialId, credential.credentialId)}
241
+ and ${exact}`;
242
+ }).pipe(Effect.flatten);
243
+ }).pipe(failure),
244
+ } satisfies Pick<PasswordPersistence["Service"], "findCredential" | "rehashIfCurrent">;
245
+ });