@yielded/auth-persistence 0.1.0-beta.10

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 +27 -0
  2. package/dist/Adapter.d.mts +30 -0
  3. package/dist/Adapter.mjs +25 -0
  4. package/dist/index.d.mts +7 -0
  5. package/dist/index.mjs +7 -0
  6. package/dist/internal/PasswordPreparedPostconditions.d.mts +22 -0
  7. package/dist/internal/PasswordPreparedPostconditions.mjs +12 -0
  8. package/dist/internal/PasswordPreparedPostconditions.mjs.map +1 -0
  9. package/dist/internal/configuration.d.mts +174 -0
  10. package/dist/internal/configuration.mjs +7 -0
  11. package/dist/internal/configuration.mjs.map +1 -0
  12. package/dist/internal/email-kernel.d.mts +275 -0
  13. package/dist/internal/email-kernel.mjs +487 -0
  14. package/dist/internal/email-kernel.mjs.map +1 -0
  15. package/dist/internal/mapping-error.d.mts +10 -0
  16. package/dist/internal/mapping-error.mjs +30 -0
  17. package/dist/internal/mapping-error.mjs.map +1 -0
  18. package/dist/internal/models/common.d.mts +12 -0
  19. package/dist/internal/models/email-model.d.mts +353 -0
  20. package/dist/internal/models/email-model.mjs +21 -0
  21. package/dist/internal/models/email-model.mjs.map +1 -0
  22. package/dist/internal/models/passkey-model.d.mts +266 -0
  23. package/dist/internal/models/passkey-model.mjs +34 -0
  24. package/dist/internal/models/passkey-model.mjs.map +1 -0
  25. package/dist/internal/models/passkey-registration-ceremony-model.d.mts +49 -0
  26. package/dist/internal/models/passkey-registration-ceremony-model.mjs +18 -0
  27. package/dist/internal/models/passkey-registration-ceremony-model.mjs.map +1 -0
  28. package/dist/internal/models/passkey-write-model.d.mts +189 -0
  29. package/dist/internal/models/passkey-write-model.mjs +13 -0
  30. package/dist/internal/models/passkey-write-model.mjs.map +1 -0
  31. package/dist/internal/models/password-model.d.mts +328 -0
  32. package/dist/internal/models/password-model.mjs +23 -0
  33. package/dist/internal/models/password-model.mjs.map +1 -0
  34. package/dist/internal/models/proof-model.d.mts +285 -0
  35. package/dist/internal/models/proof-model.mjs +17 -0
  36. package/dist/internal/models/proof-model.mjs.map +1 -0
  37. package/dist/internal/models/session-model.d.mts +199 -0
  38. package/dist/internal/models/session-model.mjs +20 -0
  39. package/dist/internal/models/session-model.mjs.map +1 -0
  40. package/dist/internal/models/step-up-model.d.mts +66 -0
  41. package/dist/internal/models/step-up-model.mjs +9 -0
  42. package/dist/internal/models/step-up-model.mjs.map +1 -0
  43. package/dist/internal/oauth-app.d.mts +11 -0
  44. package/dist/internal/oauth-app.mjs +57 -0
  45. package/dist/internal/oauth-app.mjs.map +1 -0
  46. package/dist/internal/oauth-server.d.mts +10 -0
  47. package/dist/internal/oauth-server.mjs +66 -0
  48. package/dist/internal/oauth-server.mjs.map +1 -0
  49. package/dist/internal/passkey/admission.d.mts +6 -0
  50. package/dist/internal/passkey/admission.mjs +216 -0
  51. package/dist/internal/passkey/admission.mjs.map +1 -0
  52. package/dist/internal/passkey/credentials.d.mts +14 -0
  53. package/dist/internal/passkey/credentials.mjs +249 -0
  54. package/dist/internal/passkey/credentials.mjs.map +1 -0
  55. package/dist/internal/passkey/enrollment.mjs +138 -0
  56. package/dist/internal/passkey/enrollment.mjs.map +1 -0
  57. package/dist/internal/passkey/flow.d.mts +21 -0
  58. package/dist/internal/passkey/flow.mjs +452 -0
  59. package/dist/internal/passkey/flow.mjs.map +1 -0
  60. package/dist/internal/passkey/management.mjs +231 -0
  61. package/dist/internal/passkey/management.mjs.map +1 -0
  62. package/dist/internal/passkey/registration-ceremony.d.mts +9 -0
  63. package/dist/internal/passkey/registration-ceremony.mjs +129 -0
  64. package/dist/internal/passkey/registration-ceremony.mjs.map +1 -0
  65. package/dist/internal/passkey/registration-custody.d.mts +2 -0
  66. package/dist/internal/passkey/registration-custody.mjs +52 -0
  67. package/dist/internal/passkey/registration-custody.mjs.map +1 -0
  68. package/dist/internal/passkey/registration-write.mjs +211 -0
  69. package/dist/internal/passkey/registration-write.mjs.map +1 -0
  70. package/dist/internal/passkey/state.d.mts +10 -0
  71. package/dist/internal/passkey/state.mjs +234 -0
  72. package/dist/internal/passkey/state.mjs.map +1 -0
  73. package/dist/internal/passkey/target.d.mts +19 -0
  74. package/dist/internal/passkey/target.mjs +190 -0
  75. package/dist/internal/passkey/target.mjs.map +1 -0
  76. package/dist/internal/passkey/write-state.d.mts +17 -0
  77. package/dist/internal/passkey/write-state.mjs +345 -0
  78. package/dist/internal/passkey/write-state.mjs.map +1 -0
  79. package/dist/internal/passkey/write-target.mjs +192 -0
  80. package/dist/internal/passkey/write-target.mjs.map +1 -0
  81. package/dist/internal/passkey-kernel.d.mts +5592 -0
  82. package/dist/internal/passkey-kernel.mjs +49 -0
  83. package/dist/internal/passkey-kernel.mjs.map +1 -0
  84. package/dist/internal/passkeys.d.mts +7 -0
  85. package/dist/internal/passkeys.mjs +368 -0
  86. package/dist/internal/passkeys.mjs.map +1 -0
  87. package/dist/internal/password-kernel.d.mts +290 -0
  88. package/dist/internal/password-kernel.mjs +704 -0
  89. package/dist/internal/password-kernel.mjs.map +1 -0
  90. package/dist/internal/persistence.d.mts +25 -0
  91. package/dist/internal/persistence.mjs +291 -0
  92. package/dist/internal/persistence.mjs.map +1 -0
  93. package/dist/internal/phone-kernel.d.mts +162 -0
  94. package/dist/internal/phone-kernel.mjs +434 -0
  95. package/dist/internal/phone-kernel.mjs.map +1 -0
  96. package/dist/internal/proof-kernel.d.mts +93 -0
  97. package/dist/internal/proof-kernel.mjs +619 -0
  98. package/dist/internal/proof-kernel.mjs.map +1 -0
  99. package/dist/internal/query-operations.d.mts +61 -0
  100. package/dist/internal/registration-contract.d.mts +16 -0
  101. package/dist/internal/registration.mjs +66 -0
  102. package/dist/internal/registration.mjs.map +1 -0
  103. package/dist/internal/session-kernel.d.mts +48 -0
  104. package/dist/internal/session-kernel.mjs +737 -0
  105. package/dist/internal/session-kernel.mjs.map +1 -0
  106. package/dist/internal/sql-persistence.d.mts +10 -0
  107. package/dist/internal/sql-persistence.mjs +37 -0
  108. package/dist/internal/sql-persistence.mjs.map +1 -0
  109. package/dist/internal/sql-query.mjs +207 -0
  110. package/dist/internal/sql-query.mjs.map +1 -0
  111. package/dist/internal/sql-table.d.mts +36 -0
  112. package/dist/internal/sql-table.mjs +81 -0
  113. package/dist/internal/sql-table.mjs.map +1 -0
  114. package/dist/internal/standalone.d.mts +9 -0
  115. package/dist/internal/standalone.mjs +13 -0
  116. package/dist/internal/standalone.mjs.map +1 -0
  117. package/dist/internal/step-up-state.d.mts +49 -0
  118. package/dist/internal/step-up-state.mjs +32 -0
  119. package/dist/internal/step-up-state.mjs.map +1 -0
  120. package/dist/internal/storage-mapping.mjs +686 -0
  121. package/dist/internal/storage-mapping.mjs.map +1 -0
  122. package/dist/internal/storage-tables.d.mts +913 -0
  123. package/dist/internal/storage-tables.mjs +387 -0
  124. package/dist/internal/storage-tables.mjs.map +1 -0
  125. package/dist/internal/storage-validation.mjs +58 -0
  126. package/dist/internal/storage-validation.mjs.map +1 -0
  127. package/dist/internal/transaction-execution-kernel.d.mts +38 -0
  128. package/dist/internal/transaction-execution-kernel.mjs +116 -0
  129. package/dist/internal/transaction-execution-kernel.mjs.map +1 -0
  130. package/dist/internal/transaction-kernel.d.mts +77 -0
  131. package/dist/internal/transaction-kernel.mjs +260 -0
  132. package/dist/internal/transaction-kernel.mjs.map +1 -0
  133. package/package.json +1 -0
  134. package/src/Adapter.ts +291 -0
  135. package/src/index.ts +6 -0
  136. package/src/internal/PasswordPreparedPostconditions.ts +30 -0
  137. package/src/internal/configuration.ts +282 -0
  138. package/src/internal/email-kernel.ts +1535 -0
  139. package/src/internal/mapping-error.ts +36 -0
  140. package/src/internal/models/common.ts +11 -0
  141. package/src/internal/models/email-model.ts +559 -0
  142. package/src/internal/models/passkey-model.ts +390 -0
  143. package/src/internal/models/passkey-registration-ceremony-model.ts +72 -0
  144. package/src/internal/models/passkey-write-model.ts +297 -0
  145. package/src/internal/models/password-model.ts +513 -0
  146. package/src/internal/models/proof-model.ts +424 -0
  147. package/src/internal/models/session-model.ts +375 -0
  148. package/src/internal/models/step-up-model.ts +135 -0
  149. package/src/internal/oauth-app.ts +82 -0
  150. package/src/internal/oauth-server.ts +89 -0
  151. package/src/internal/passkey/admission.ts +361 -0
  152. package/src/internal/passkey/credentials.ts +511 -0
  153. package/src/internal/passkey/enrollment.ts +326 -0
  154. package/src/internal/passkey/flow.ts +1062 -0
  155. package/src/internal/passkey/management.ts +421 -0
  156. package/src/internal/passkey/registration-ceremony.ts +283 -0
  157. package/src/internal/passkey/registration-custody.ts +91 -0
  158. package/src/internal/passkey/registration-write.ts +426 -0
  159. package/src/internal/passkey/state.ts +353 -0
  160. package/src/internal/passkey/target.ts +586 -0
  161. package/src/internal/passkey/write-state.ts +681 -0
  162. package/src/internal/passkey/write-target.ts +452 -0
  163. package/src/internal/passkey-kernel.ts +104 -0
  164. package/src/internal/passkeys.ts +606 -0
  165. package/src/internal/password-kernel.ts +2124 -0
  166. package/src/internal/persistence.ts +588 -0
  167. package/src/internal/phone-kernel.ts +846 -0
  168. package/src/internal/proof-kernel.ts +1954 -0
  169. package/src/internal/query-operations.ts +81 -0
  170. package/src/internal/registration-contract.ts +22 -0
  171. package/src/internal/registration.ts +161 -0
  172. package/src/internal/session-kernel.ts +2217 -0
  173. package/src/internal/sql-persistence.ts +45 -0
  174. package/src/internal/sql-query.ts +298 -0
  175. package/src/internal/sql-table.ts +104 -0
  176. package/src/internal/standalone.ts +30 -0
  177. package/src/internal/step-up-state.ts +139 -0
  178. package/src/internal/storage-mapping.ts +708 -0
  179. package/src/internal/storage-tables.ts +408 -0
  180. package/src/internal/storage-validation.ts +117 -0
  181. package/src/internal/transaction-execution-kernel.ts +313 -0
  182. package/src/internal/transaction-kernel.ts +593 -0
@@ -0,0 +1,375 @@
1
+ import type { TokenDigest } from "@yielded/auth/Schema";
2
+ import {
3
+ type AuthenticationRequirement,
4
+ type AuthenticationEvidence,
5
+ type SecurityRevision,
6
+ type SessionId,
7
+ type PendingAuthenticationRecord,
8
+ type PendingAuthenticationState,
9
+ type StatefulSessionRecord,
10
+ } from "@yielded/auth/Sessions";
11
+ import type { DateTime, Effect } from "effect";
12
+
13
+ import type { TableModel as Table, SqlExpression } from "../query-operations";
14
+ import type { PersistenceMappingError, SubjectIdCodec } from "./common";
15
+
16
+ type ColumnKey<T extends Table> = T["column"];
17
+
18
+ export interface SessionIdCodec<NativeId> {
19
+ readonly toNative: (id: SessionId) => Effect.Effect<NativeId, PersistenceMappingError>;
20
+ readonly toSession: (id: NativeId) => Effect.Effect<SessionId, PersistenceMappingError>;
21
+ readonly equals: (left: NativeId, right: NativeId) => boolean;
22
+ }
23
+
24
+ export interface SessionSubjectTables<Subject extends Table, NativeId> {
25
+ readonly subject: {
26
+ readonly table: Subject["table"];
27
+ readonly id: ColumnKey<Subject>;
28
+ readonly status: ColumnKey<Subject>;
29
+ readonly securityRevision: ColumnKey<Subject>;
30
+ readonly isActiveStatus: (value: unknown) => boolean;
31
+ readonly decodeRequirement: (
32
+ row: Subject["select"],
33
+ ) => Effect.Effect<AuthenticationRequirement, PersistenceMappingError>;
34
+ readonly nextSecurityRevision?: (
35
+ current: SecurityRevision,
36
+ ) => Effect.Effect<SecurityRevision, PersistenceMappingError>;
37
+ readonly nextSecurityRevisionSync?: (current: SecurityRevision) => SecurityRevision;
38
+ };
39
+ readonly subjectId: SubjectIdCodec<NativeId>;
40
+ }
41
+
42
+ export interface SessionAuthorityTables<
43
+ Subject extends Table,
44
+ Credential extends Table,
45
+ NativeId,
46
+ > extends SessionSubjectTables<Subject, NativeId> {
47
+ readonly credential: {
48
+ readonly table: Credential["table"];
49
+ readonly subjectId: ColumnKey<Credential>;
50
+ readonly credentialId: ColumnKey<Credential>;
51
+ readonly revision: ColumnKey<Credential>;
52
+ /** Optional consumer status predicate; omitted means every stored credential row is active. */
53
+ readonly status?: ColumnKey<Credential>;
54
+ readonly isActiveStatus?: (value: unknown) => boolean;
55
+ };
56
+ }
57
+
58
+ export interface SessionFlowTables<Flow extends Table, NativeSubjectId> {
59
+ readonly flow: {
60
+ readonly table: Flow["table"];
61
+ readonly flowId: ColumnKey<Flow>;
62
+ readonly subjectId: ColumnKey<Flow>;
63
+ readonly state: ColumnKey<Flow>;
64
+ readonly pendingDigest: ColumnKey<Flow>;
65
+ readonly dedupUntil: ColumnKey<Flow>;
66
+ readonly pendingStateValue: unknown;
67
+ readonly establishedStateValue: unknown;
68
+ readonly encodeInstant: (instant: DateTime.Utc) => unknown;
69
+ readonly decodeInstant: (
70
+ native: unknown,
71
+ ) => Effect.Effect<DateTime.Utc, PersistenceMappingError>;
72
+ readonly encodePendingInsert: (input: {
73
+ readonly evidence: AuthenticationEvidence;
74
+ readonly subjectId: NativeSubjectId;
75
+ readonly pendingDigest: TokenDigest;
76
+ readonly dedupUntil: DateTime.Utc;
77
+ }) => Flow["insert"];
78
+ readonly encodeEstablishedInsert: (input: {
79
+ readonly evidence: AuthenticationEvidence;
80
+ readonly subjectId: NativeSubjectId;
81
+ readonly dedupUntil: DateTime.Utc;
82
+ }) => Flow["insert"];
83
+ };
84
+ }
85
+
86
+ export interface StatefulSessionTables<
87
+ Claims,
88
+ Session extends Table,
89
+ Flow extends Table,
90
+ NativeSubjectId,
91
+ NativeSessionId,
92
+ > extends SessionFlowTables<Flow, NativeSubjectId> {
93
+ readonly session: {
94
+ readonly table: Session["table"];
95
+ readonly sessionId: ColumnKey<Session>;
96
+ readonly subjectId: ColumnKey<Session>;
97
+ readonly digest: ColumnKey<Session>;
98
+ readonly version: ColumnKey<Session>;
99
+ readonly securityRevision: ColumnKey<Session>;
100
+ readonly issuedAt: ColumnKey<Session>;
101
+ readonly expiresAt: ColumnKey<Session>;
102
+ readonly absoluteExpiresAt: ColumnKey<Session>;
103
+ readonly encodeInstant: (instant: DateTime.Utc) => unknown;
104
+ readonly encodeInsert: (
105
+ record: StatefulSessionRecord<Claims>,
106
+ ids: {
107
+ readonly subjectId: NativeSubjectId;
108
+ readonly sessionId: NativeSessionId;
109
+ },
110
+ ) => Session["insert"];
111
+ /** Persist the prepared credentialVersion and retain exact private provenance. */
112
+ readonly encodeRotation: (record: StatefulSessionRecord<Claims>) => Partial<Session["insert"]>;
113
+ readonly decode: (
114
+ row: Session["select"],
115
+ ) => Effect.Effect<StatefulSessionRecord<Claims>, PersistenceMappingError>;
116
+ readonly allocateId?: Effect.Effect<NativeSessionId, PersistenceMappingError>;
117
+ readonly allocateIdSync?: () => NativeSessionId;
118
+ readonly allocateVersion?: Effect.Effect<SecurityRevision, PersistenceMappingError>;
119
+ readonly allocateVersionSync?: () => SecurityRevision;
120
+ };
121
+ readonly sessionId: SessionIdCodec<NativeSessionId>;
122
+ }
123
+
124
+ export interface PendingAuthenticationTables<
125
+ Claims,
126
+ Pending extends Table,
127
+ Flow extends Table,
128
+ NativeSubjectId,
129
+ > extends SessionFlowTables<Flow, NativeSubjectId> {
130
+ readonly pending: {
131
+ readonly table: Pending["table"];
132
+ readonly digest: ColumnKey<Pending>;
133
+ readonly version: ColumnKey<Pending>;
134
+ readonly flowId: ColumnKey<Pending>;
135
+ readonly subjectId: ColumnKey<Pending>;
136
+ readonly bindingDigest: ColumnKey<Pending>;
137
+ readonly expiresAt: ColumnKey<Pending>;
138
+ readonly attemptLimit: ColumnKey<Pending>;
139
+ readonly failedAttempts: ColumnKey<Pending>;
140
+ readonly consumed: ColumnKey<Pending>;
141
+ readonly encodeInstant: (instant: DateTime.Utc) => unknown;
142
+ readonly encodeInsert: (
143
+ record: PendingAuthenticationRecord<Claims>,
144
+ input: {
145
+ readonly subjectId: NativeSubjectId;
146
+ readonly failedAttempts: 0;
147
+ readonly consumed: false;
148
+ },
149
+ ) => Pending["insert"];
150
+ readonly decode: (
151
+ row: Pending["select"],
152
+ ) => Effect.Effect<PendingAuthenticationRecord<Claims>, PersistenceMappingError>;
153
+ /** Decodes original evidence and storage guards only. The internal pending
154
+ * context lookup must not execute unrelated consumer Claims transforms. */
155
+ readonly decodeContext: (
156
+ row: Pending["select"],
157
+ ) => Effect.Effect<PendingAuthenticationState, PersistenceMappingError>;
158
+ readonly allocateVersion?: Effect.Effect<SecurityRevision, PersistenceMappingError>;
159
+ readonly allocateVersionSync?: () => SecurityRevision;
160
+ };
161
+ }
162
+
163
+ export interface SignedSessionValidityTables<
164
+ Tombstone extends Table,
165
+ NativeSubjectId,
166
+ NativeSessionId,
167
+ > {
168
+ readonly tombstone: {
169
+ readonly table: Tombstone["table"];
170
+ readonly subjectId: ColumnKey<Tombstone>;
171
+ readonly sessionId: ColumnKey<Tombstone>;
172
+ readonly absoluteExpiresAt: ColumnKey<Tombstone>;
173
+ readonly encodeInstant: (instant: DateTime.Utc) => unknown;
174
+ readonly decodeInstant: (
175
+ native: unknown,
176
+ ) => Effect.Effect<DateTime.Utc, PersistenceMappingError>;
177
+ readonly encodeInsert: (input: {
178
+ readonly subjectId: NativeSubjectId;
179
+ readonly sessionId: NativeSessionId;
180
+ readonly absoluteExpiresAt: DateTime.Utc;
181
+ }) => Tombstone["insert"];
182
+ };
183
+ readonly sessionId: SessionIdCodec<NativeSessionId>;
184
+ }
185
+
186
+ export interface RequiredSessionConstraints {
187
+ readonly sessionDigest: "unique(session.digest)";
188
+ readonly flowId: "unique(flow.flowId)";
189
+ }
190
+
191
+ export interface RequiredPendingAuthenticationConstraints {
192
+ readonly pendingDigest: "unique(pending.digest)";
193
+ readonly pendingFlow: "unique(pending.flowId)";
194
+ readonly flowId: "unique(flow.flowId)";
195
+ }
196
+
197
+ export interface RequiredStatefulPendingConstraints
198
+ extends RequiredSessionConstraints, RequiredPendingAuthenticationConstraints {}
199
+
200
+ export interface RequiredSignedValidityConstraints {
201
+ readonly tombstoneOwner: "unique(tombstone.subjectId,tombstone.sessionId)";
202
+ }
203
+
204
+ export const requiredSessionConstraints: RequiredSessionConstraints = {
205
+ sessionDigest: "unique(session.digest)",
206
+ flowId: "unique(flow.flowId)",
207
+ };
208
+
209
+ export const requiredPendingAuthenticationConstraints: RequiredPendingAuthenticationConstraints = {
210
+ pendingDigest: "unique(pending.digest)",
211
+ pendingFlow: "unique(pending.flowId)",
212
+ flowId: "unique(flow.flowId)",
213
+ };
214
+
215
+ export const requiredStatefulPendingConstraints: RequiredStatefulPendingConstraints = {
216
+ ...requiredSessionConstraints,
217
+ ...requiredPendingAuthenticationConstraints,
218
+ };
219
+
220
+ export const requiredSignedValidityConstraints: RequiredSignedValidityConstraints = {
221
+ tombstoneOwner: "unique(tombstone.subjectId,tombstone.sessionId)",
222
+ };
223
+
224
+ export interface SessionConstraintClassifier {
225
+ /**
226
+ * Match only the named unique constraints declared by this mapping. Broad
227
+ * SQL/constraint classifiers can relabel application or NOT NULL failures as
228
+ * a domain conflict.
229
+ */
230
+ readonly isConstraintConflict: (cause: unknown) => boolean;
231
+ }
232
+
233
+ export type AuthenticationAuthorityMapping<
234
+ Claims,
235
+ Subject extends Table,
236
+ Credential extends Table,
237
+ Flow extends Table,
238
+ Pending extends Table,
239
+ NativeSubjectId,
240
+ > = SessionAuthorityTables<Subject, Credential, NativeSubjectId> &
241
+ SessionConstraintClassifier &
242
+ (
243
+ | {
244
+ /** Omit for single-factor authority installations. Pending input then fails closed. */
245
+ readonly pending?: undefined;
246
+ }
247
+ | {
248
+ readonly pending: PendingAuthenticationTables<Claims, Pending, Flow, NativeSubjectId>;
249
+ readonly constraints: RequiredPendingAuthenticationConstraints;
250
+ }
251
+ );
252
+
253
+ export type StatefulSessionMapping<
254
+ Claims,
255
+ Subject extends Table,
256
+ Credential extends Table,
257
+ Session extends Table,
258
+ Flow extends Table,
259
+ Pending extends Table,
260
+ NativeSubjectId,
261
+ NativeSessionId,
262
+ > = SessionAuthorityTables<Subject, Credential, NativeSubjectId> &
263
+ StatefulSessionTables<Claims, Session, Flow, NativeSubjectId, NativeSessionId> &
264
+ SessionConstraintClassifier &
265
+ (
266
+ | {
267
+ /** Omit when this session module does not support pending MFA completion. */
268
+ readonly pending?: undefined;
269
+ readonly constraints: RequiredSessionConstraints;
270
+ }
271
+ | {
272
+ readonly pending: PendingAuthenticationTables<
273
+ Claims,
274
+ Pending,
275
+ Flow,
276
+ NativeSubjectId
277
+ >["pending"];
278
+ readonly constraints: RequiredStatefulPendingConstraints;
279
+ }
280
+ );
281
+
282
+ export type PendingAuthenticationMapping<
283
+ Claims,
284
+ Subject extends Table,
285
+ Credential extends Table,
286
+ Pending extends Table,
287
+ Flow extends Table,
288
+ NativeSubjectId,
289
+ > = SessionAuthorityTables<Subject, Credential, NativeSubjectId> &
290
+ PendingAuthenticationTables<Claims, Pending, Flow, NativeSubjectId> &
291
+ SessionConstraintClassifier & {
292
+ readonly constraints: RequiredPendingAuthenticationConstraints;
293
+ };
294
+
295
+ export type SignedSessionValidityMapping<
296
+ Subject extends Table,
297
+ Tombstone extends Table,
298
+ NativeSubjectId,
299
+ NativeSessionId,
300
+ > = SessionSubjectTables<Subject, NativeSubjectId> &
301
+ SignedSessionValidityTables<Tombstone, NativeSubjectId, NativeSessionId> &
302
+ SessionConstraintClassifier & {
303
+ readonly constraints: RequiredSignedValidityConstraints;
304
+ };
305
+
306
+ /** D1 evaluates these expressions inside the atomic batch, after queueing delay.
307
+ * `engineNow` must use the exact native representation of every mapped instant
308
+ * column. `engineNowMillis` is Unix epoch milliseconds for proof freshness.
309
+ * Changes to the consumer's factor policy MUST bump subject.securityRevision.
310
+ * Supply the authoritative primary D1 database, never a replica-affined
311
+ * `withSession` handle, because verification and security revision reads must
312
+ * observe completed revocation writes across clients.
313
+ */
314
+ export interface D1SessionClockMapping<Expression extends SqlExpression = SqlExpression> {
315
+ readonly d1: {
316
+ readonly engineNow: Expression;
317
+ readonly engineNowMillis: Expression;
318
+ readonly activeSubjectStatusValue: unknown;
319
+ /** Required when `credential.status` is mapped. */
320
+ readonly activeCredentialStatusValue?: unknown;
321
+ };
322
+ }
323
+
324
+ export type D1AuthenticationAuthorityMapping<
325
+ Claims,
326
+ Subject extends Table,
327
+ Credential extends Table,
328
+ Flow extends Table,
329
+ Pending extends Table,
330
+ NativeSubjectId,
331
+ Expression extends SqlExpression = SqlExpression,
332
+ > = AuthenticationAuthorityMapping<Claims, Subject, Credential, Flow, Pending, NativeSubjectId> &
333
+ D1SessionClockMapping<Expression>;
334
+
335
+ export type D1PendingAuthenticationMapping<
336
+ Claims,
337
+ Subject extends Table,
338
+ Credential extends Table,
339
+ Pending extends Table,
340
+ Flow extends Table,
341
+ NativeSubjectId,
342
+ Expression extends SqlExpression = SqlExpression,
343
+ > = PendingAuthenticationMapping<Claims, Subject, Credential, Pending, Flow, NativeSubjectId> &
344
+ D1SessionClockMapping<Expression>;
345
+
346
+ export type D1StatefulSessionMapping<
347
+ Claims,
348
+ Subject extends Table,
349
+ Credential extends Table,
350
+ Session extends Table,
351
+ Flow extends Table,
352
+ Pending extends Table,
353
+ NativeSubjectId,
354
+ NativeSessionId,
355
+ Expression extends SqlExpression = SqlExpression,
356
+ > = StatefulSessionMapping<
357
+ Claims,
358
+ Subject,
359
+ Credential,
360
+ Session,
361
+ Flow,
362
+ Pending,
363
+ NativeSubjectId,
364
+ NativeSessionId
365
+ > &
366
+ D1SessionClockMapping<Expression>;
367
+
368
+ export type D1SignedSessionValidityMapping<
369
+ Subject extends Table,
370
+ Tombstone extends Table,
371
+ NativeSubjectId,
372
+ NativeSessionId,
373
+ Expression extends SqlExpression = SqlExpression,
374
+ > = SignedSessionValidityMapping<Subject, Tombstone, NativeSubjectId, NativeSessionId> &
375
+ D1SessionClockMapping<Expression>;
@@ -0,0 +1,135 @@
1
+ import type { SecurityRevision, SessionStepUpIntent } from "@yielded/auth/Sessions";
2
+ import type { DateTime, Effect } from "effect";
3
+
4
+ import type { TableModel as Table, SqlExpression } from "../query-operations";
5
+ import type { PersistenceMappingError } from "./common";
6
+ import type {
7
+ D1SessionClockMapping,
8
+ SessionAuthorityTables,
9
+ SessionConstraintClassifier,
10
+ SessionIdCodec,
11
+ SignedSessionValidityTables,
12
+ StatefulSessionTables,
13
+ } from "./session-model";
14
+
15
+ type ColumnKey<T extends Table> = T["column"];
16
+
17
+ /** Distinct from login-pending storage. Snapshot is TEXT encoded by the core's
18
+ * SessionStepUpIntent schema; it contains no Claims and needs no consumer decoder. */
19
+ export interface SessionStepUpIntentTables<Intent extends Table, NativeSubjectId> {
20
+ readonly intent: {
21
+ readonly table: Intent["table"];
22
+ readonly digest: ColumnKey<Intent>;
23
+ readonly version: ColumnKey<Intent>;
24
+ readonly flowId: ColumnKey<Intent>;
25
+ readonly subjectId: ColumnKey<Intent>;
26
+ readonly bindingDigest: ColumnKey<Intent>;
27
+ readonly snapshot: ColumnKey<Intent>;
28
+ readonly expiresAt: ColumnKey<Intent>;
29
+ readonly attemptLimit: ColumnKey<Intent>;
30
+ readonly failedAttempts: ColumnKey<Intent>;
31
+ readonly consumed: ColumnKey<Intent>;
32
+ readonly encodeInstant: (instant: DateTime.Utc) => unknown;
33
+ /** Consumer fields/defaults; the adapter sets all mapped security columns. */
34
+ readonly encodeInsert: (
35
+ intent: SessionStepUpIntent,
36
+ input: {
37
+ readonly subjectId: NativeSubjectId;
38
+ },
39
+ ) => Intent["insert"];
40
+ readonly allocateVersion?: Effect.Effect<SecurityRevision, PersistenceMappingError>;
41
+ readonly allocateVersionSync?: () => SecurityRevision;
42
+ };
43
+ }
44
+
45
+ export interface RequiredSessionStepUpConstraints {
46
+ readonly intentDigest: "unique(intent.digest)";
47
+ readonly intentFlow: "unique(intent.flowId)";
48
+ }
49
+
50
+ export const requiredSessionStepUpConstraints: RequiredSessionStepUpConstraints = {
51
+ intentDigest: "unique(intent.digest)",
52
+ intentFlow: "unique(intent.flowId)",
53
+ };
54
+
55
+ /** Fixed source discriminator is checked against every stored intent. A minimal
56
+ * pure-signed installation does not provide dummy session or tombstone tables. */
57
+ export type SessionStepUpSourceTables<
58
+ Claims,
59
+ Session extends Table,
60
+ Tombstone extends Table,
61
+ NativeSubjectId,
62
+ NativeSessionId,
63
+ > =
64
+ | {
65
+ readonly kind: "StatelessSigned";
66
+ }
67
+ | ({
68
+ readonly kind: "StateAssistedSigned";
69
+ readonly constraints: {
70
+ readonly tombstoneOwner: "unique(tombstone.subjectId,tombstone.sessionId)";
71
+ };
72
+ } & SignedSessionValidityTables<Tombstone, NativeSubjectId, NativeSessionId>)
73
+ | {
74
+ readonly kind: "Stateful";
75
+ readonly constraints: {
76
+ readonly sessionDigest: "unique(session.digest)";
77
+ };
78
+ readonly sessionId: SessionIdCodec<NativeSessionId>;
79
+ /** encodeRotation must replace Claims, provenance, and the queryable security
80
+ * fields together; it must not retain the old proof payload. */
81
+ readonly session: StatefulSessionTables<
82
+ Claims,
83
+ Session,
84
+ Table,
85
+ NativeSubjectId,
86
+ NativeSessionId
87
+ >["session"] & {
88
+ readonly credentialVersion: ColumnKey<Session>;
89
+ readonly authenticatedAt: ColumnKey<Session>;
90
+ };
91
+ };
92
+
93
+ export type SessionStepUpMapping<
94
+ Claims,
95
+ Subject extends Table,
96
+ Credential extends Table,
97
+ Intent extends Table,
98
+ Session extends Table,
99
+ Tombstone extends Table,
100
+ NativeSubjectId,
101
+ NativeSessionId,
102
+ > = SessionAuthorityTables<Subject, Credential, NativeSubjectId> &
103
+ SessionStepUpIntentTables<Intent, NativeSubjectId> &
104
+ SessionConstraintClassifier & {
105
+ readonly source: SessionStepUpSourceTables<
106
+ Claims,
107
+ Session,
108
+ Tombstone,
109
+ NativeSubjectId,
110
+ NativeSessionId
111
+ >;
112
+ readonly constraints: RequiredSessionStepUpConstraints;
113
+ };
114
+
115
+ export type D1SessionStepUpMapping<
116
+ Claims,
117
+ Subject extends Table,
118
+ Credential extends Table,
119
+ Intent extends Table,
120
+ Session extends Table,
121
+ Tombstone extends Table,
122
+ NativeSubjectId,
123
+ NativeSessionId,
124
+ Expression extends SqlExpression = SqlExpression,
125
+ > = SessionStepUpMapping<
126
+ Claims,
127
+ Subject,
128
+ Credential,
129
+ Intent,
130
+ Session,
131
+ Tombstone,
132
+ NativeSubjectId,
133
+ NativeSessionId
134
+ > &
135
+ D1SessionClockMapping<Expression>;
@@ -0,0 +1,82 @@
1
+ import { OAuthUnavailable } from "@yielded/auth/OAuth";
2
+ import { Persistence, Record } from "@yielded/auth/OAuthApp";
3
+ import { Effect, Layer, Schema } from "effect";
4
+ import { SqlClient } from "effect/unstable/sql";
5
+
6
+ import { requireStandalone } from "./standalone";
7
+
8
+ const codec = Schema.fromJsonString(Record);
9
+ const Row = Schema.Struct({ payload: Schema.String, version: Schema.String });
10
+
11
+ /** Apply with the application's migrations. SQLite (including D1) and PostgreSQL.
12
+ * Each transition is one conditional statement, so D1 needs no interactive
13
+ * transaction and parallel workers share the same refresh/code claim.
14
+ */
15
+ const migration = `CREATE TABLE yielded_oauth_app (
16
+ namespace TEXT NOT NULL,
17
+ record_key TEXT NOT NULL,
18
+ version TEXT NOT NULL,
19
+ payload TEXT NOT NULL,
20
+ PRIMARY KEY (namespace, record_key)
21
+ )`;
22
+
23
+ const layer = Layer.effect(
24
+ Persistence,
25
+ Effect.gen(function* () {
26
+ const sql = yield* SqlClient.SqlClient;
27
+
28
+ if (!sql.onDialectOrElse({ sqlite: () => true, pg: () => true, orElse: () => false }))
29
+ return yield* OAuthUnavailable.make({});
30
+
31
+ const noTransaction = requireStandalone(() => OAuthUnavailable.make({}), sql);
32
+
33
+ const failure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
34
+ effect.pipe(Effect.mapError(() => OAuthUnavailable.make({})));
35
+
36
+ return Persistence.of({
37
+ get: Effect.fn("OAuthAppPersistence.get")(function* (namespace, key) {
38
+ yield* noTransaction;
39
+
40
+ const rows = yield* sql<{
41
+ payload: string;
42
+ version: string;
43
+ }>`SELECT payload, version FROM yielded_oauth_app WHERE namespace = ${namespace} AND record_key = ${key}`;
44
+
45
+ if (rows.length === 0) return undefined;
46
+ if (rows.length !== 1) return yield* OAuthUnavailable.make({});
47
+ const row = yield* Schema.decodeEffect(Row)(rows[0]);
48
+ const value = yield* Schema.decodeEffect(codec)(row.payload);
49
+
50
+ if (value.version !== row.version) return yield* OAuthUnavailable.make({});
51
+
52
+ return value;
53
+ }, failure),
54
+ insert: Effect.fn("OAuthAppPersistence.insert")(function* (namespace, key, value) {
55
+ yield* noTransaction;
56
+ const payload = yield* Schema.encodeEffect(codec)(value);
57
+
58
+ const rows =
59
+ yield* sql`INSERT INTO yielded_oauth_app (namespace, record_key, version, payload) VALUES (${namespace}, ${key}, ${value.version}, ${payload}) ON CONFLICT (namespace, record_key) DO NOTHING RETURNING version`;
60
+
61
+ return rows.length === 1;
62
+ }, failure),
63
+ compareAndSet: Effect.fn("OAuthAppPersistence.compareAndSet")(function* (
64
+ namespace,
65
+ key,
66
+ version,
67
+ value,
68
+ ) {
69
+ yield* noTransaction;
70
+ if (value.version === version) return yield* OAuthUnavailable.make({});
71
+ const payload = yield* Schema.encodeEffect(codec)(value);
72
+
73
+ const rows =
74
+ yield* sql`UPDATE yielded_oauth_app SET version = ${value.version}, payload = ${payload} WHERE namespace = ${namespace} AND record_key = ${key} AND version = ${version} RETURNING version`;
75
+
76
+ return rows.length === 1;
77
+ }, failure),
78
+ });
79
+ }),
80
+ );
81
+
82
+ export const OAuthAppPersistence = { layer, migration };
@@ -0,0 +1,89 @@
1
+ import { Persistence, Record, Unavailable } from "@yielded/auth/OAuthServer";
2
+ import { Effect, Layer, Schema } from "effect";
3
+ import { SqlClient } from "effect/unstable/sql";
4
+
5
+ import { requireStandalone } from "./standalone";
6
+
7
+ const codec = Schema.fromJsonString(Record);
8
+
9
+ const Row = Schema.Struct({
10
+ payload: Schema.String,
11
+ version: Schema.String,
12
+ revoked: Schema.Literals([0, 1]),
13
+ });
14
+
15
+ /** SQLite (including D1) and PostgreSQL. Apply once through application migrations.
16
+ * Expired records may be deleted when expires_at_millis <= the current time.
17
+ * Revocation has its own monotonic column so it cannot lose a race with rotation.
18
+ */
19
+ const migration = `CREATE TABLE yielded_oauth_server (
20
+ namespace TEXT NOT NULL,
21
+ grant_id TEXT NOT NULL,
22
+ version TEXT NOT NULL,
23
+ revoked INTEGER NOT NULL DEFAULT 0,
24
+ expires_at_millis BIGINT NOT NULL,
25
+ payload TEXT NOT NULL,
26
+ PRIMARY KEY (namespace, grant_id)
27
+ )`;
28
+
29
+ const layer = Layer.effect(
30
+ Persistence,
31
+ Effect.gen(function* () {
32
+ const sql = yield* SqlClient.SqlClient;
33
+
34
+ if (!sql.onDialectOrElse({ sqlite: () => true, pg: () => true, orElse: () => false }))
35
+ return yield* Unavailable.make({});
36
+ const standalone = requireStandalone(() => Unavailable.make({}), sql);
37
+
38
+ const failure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
39
+ effect.pipe(Effect.mapError(() => Unavailable.make({})));
40
+
41
+ return Persistence.of({
42
+ get: Effect.fn("OAuthServerPersistence.get")(function* (namespace, id) {
43
+ yield* standalone;
44
+
45
+ const rows =
46
+ yield* sql`SELECT payload, version, revoked FROM yielded_oauth_server WHERE namespace = ${namespace} AND grant_id = ${id}`;
47
+
48
+ if (rows.length === 0) return undefined;
49
+ if (rows.length !== 1) return yield* Unavailable.make({});
50
+ const row = yield* Schema.decodeUnknownEffect(Row)(rows[0]);
51
+ const record = yield* Schema.decodeEffect(codec)(row.payload);
52
+
53
+ if (record.version !== row.version) return yield* Unavailable.make({});
54
+
55
+ return row.revoked === 1 ? { ...record, status: "Revoked" as const } : record;
56
+ }, failure),
57
+ insert: Effect.fn("OAuthServerPersistence.insert")(function* (namespace, id, record) {
58
+ yield* standalone;
59
+ const payload = yield* Schema.encodeEffect(codec)(record);
60
+
61
+ const rows =
62
+ yield* sql`INSERT INTO yielded_oauth_server (namespace, grant_id, version, revoked, expires_at_millis, payload) VALUES (${namespace}, ${id}, ${record.version}, ${record.status === "Revoked" ? 1 : 0}, ${record.expiresAtMillis}, ${payload}) ON CONFLICT (namespace, grant_id) DO NOTHING RETURNING version`;
63
+
64
+ return rows.length === 1;
65
+ }, failure),
66
+ compareAndSet: Effect.fn("OAuthServerPersistence.compareAndSet")(function* (
67
+ namespace,
68
+ id,
69
+ version,
70
+ record,
71
+ ) {
72
+ yield* standalone;
73
+ if (version === record.version) return yield* Unavailable.make({});
74
+ const payload = yield* Schema.encodeEffect(codec)(record);
75
+
76
+ const rows =
77
+ yield* sql`UPDATE yielded_oauth_server SET version = ${record.version}, revoked = ${record.status === "Revoked" ? 1 : 0}, expires_at_millis = ${record.expiresAtMillis}, payload = ${payload} WHERE namespace = ${namespace} AND grant_id = ${id} AND version = ${version} AND revoked = 0 RETURNING version`;
78
+
79
+ return rows.length === 1;
80
+ }, failure),
81
+ revoke: Effect.fn("OAuthServerPersistence.revoke")(function* (namespace, id) {
82
+ yield* standalone;
83
+ yield* sql`UPDATE yielded_oauth_server SET revoked = 1 WHERE namespace = ${namespace} AND grant_id = ${id}`;
84
+ }, failure),
85
+ });
86
+ }),
87
+ );
88
+
89
+ export const OAuthServerPersistence = { migration, layer };