@yielded/auth-persistence 0.1.0-beta.30 → 0.1.0-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/dist/Adapter.d.mts +1 -1
- package/dist/Adapter.mjs +8 -8
- package/dist/OAuthPersistence.mjs +10 -5
- package/dist/OAuthPersistence.mjs.map +1 -1
- package/dist/internal/configuration.d.mts +49 -13
- package/dist/internal/configuration.mjs.map +1 -1
- package/dist/internal/email-native.mjs +10 -2
- package/dist/internal/email-native.mjs.map +1 -1
- package/dist/internal/email-policy.mjs +3 -2
- package/dist/internal/email-policy.mjs.map +1 -1
- package/dist/internal/models/oauth-model.d.mts +4 -2
- package/dist/internal/models/oauth-model.mjs.map +1 -1
- package/dist/internal/models/password-model.d.mts +2 -1
- package/dist/internal/models/password-model.mjs.map +1 -1
- package/dist/internal/oauth/native-accounts.d.mts +1 -1
- package/dist/internal/oauth/native-accounts.mjs +6 -6
- package/dist/internal/oauth/native-accounts.mjs.map +1 -1
- package/dist/internal/oauth/native-connected.d.mts +1 -1
- package/dist/internal/oauth/native-connected.mjs +1 -1
- package/dist/internal/oauth/native-registration.mjs +19 -8
- package/dist/internal/oauth/native-registration.mjs.map +1 -1
- package/dist/internal/oauth/native-sign-in.d.mts +1 -1
- package/dist/internal/oauth/native-sign-in.mjs +1 -1
- package/dist/internal/oauth/native-state.mjs +21 -11
- package/dist/internal/oauth/native-state.mjs.map +1 -1
- package/dist/internal/oauth/state.d.mts +1 -1
- package/dist/internal/oauth/state.mjs +1 -1
- package/dist/internal/oauth-managed.mjs +182 -0
- package/dist/internal/oauth-managed.mjs.map +1 -0
- package/dist/internal/passkey-managed.mjs +5 -3
- package/dist/internal/passkey-managed.mjs.map +1 -1
- package/dist/internal/passkey-native-registration.mjs +1 -1
- package/dist/internal/passkey-native-state.mjs +33 -23
- package/dist/internal/passkey-native-state.mjs.map +1 -1
- package/dist/internal/passkey-native-write.mjs +3 -3
- package/dist/internal/passkey-native-write.mjs.map +1 -1
- package/dist/internal/passkey-native.d.mts +15 -7
- package/dist/internal/passkey-native.mjs +2 -1
- package/dist/internal/passkey-native.mjs.map +1 -1
- package/dist/internal/passkey-policy.mjs +9 -7
- package/dist/internal/passkey-policy.mjs.map +1 -1
- package/dist/internal/password-native.mjs +22 -5
- package/dist/internal/password-native.mjs.map +1 -1
- package/dist/internal/password-policy.mjs +7 -6
- package/dist/internal/password-policy.mjs.map +1 -1
- package/dist/internal/password-registration-native.d.mts +1 -1
- package/dist/internal/persistence.d.mts +3 -0
- package/dist/internal/persistence.mjs +99 -42
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/phone-native.mjs +1 -1
- package/dist/internal/phone-native.mjs.map +1 -1
- package/dist/internal/registration.mjs +17 -8
- package/dist/internal/registration.mjs.map +1 -1
- package/dist/internal/session-native-authority.mjs +8 -6
- package/dist/internal/session-native-authority.mjs.map +1 -1
- package/dist/internal/session-native-login.d.mts +2 -2
- package/dist/internal/session-native-login.mjs +4 -2
- package/dist/internal/session-native-login.mjs.map +1 -1
- package/dist/internal/session-native-pending-read.mjs +26 -20
- package/dist/internal/session-native-pending-read.mjs.map +1 -1
- package/dist/internal/session-native-record.mjs +6 -4
- package/dist/internal/session-native-record.mjs.map +1 -1
- package/dist/internal/session-native-state.mjs +24 -8
- package/dist/internal/session-native-state.mjs.map +1 -1
- package/dist/internal/session-native-stateful.d.mts +2 -2
- package/dist/internal/session-native-stateful.mjs +67 -38
- package/dist/internal/session-native-stateful.mjs.map +1 -1
- package/dist/internal/session-native-step-up.d.mts +2 -2
- package/dist/internal/session-native-step-up.mjs +17 -19
- package/dist/internal/session-native-step-up.mjs.map +1 -1
- package/dist/internal/session-native-validity.mjs +3 -1
- package/dist/internal/session-native-validity.mjs.map +1 -1
- package/dist/internal/sql-authority.mjs +5 -1
- package/dist/internal/sql-authority.mjs.map +1 -1
- package/dist/internal/sql-storage-validation.mjs +2 -1
- package/dist/internal/sql-storage-validation.mjs.map +1 -1
- package/dist/internal/sql-table.d.mts +1 -0
- package/dist/internal/sql-table.mjs.map +1 -1
- package/dist/internal/sqlite-policy-guard.mjs +23 -0
- package/dist/internal/sqlite-policy-guard.mjs.map +1 -0
- package/dist/internal/step-up-state.mjs +19 -6
- package/dist/internal/step-up-state.mjs.map +1 -1
- package/dist/internal/storage-mapping.d.mts +9 -5
- package/dist/internal/storage-mapping.mjs +17 -5
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/dist/internal/storage-tables.d.mts +237 -0
- package/dist/internal/storage-tables.mjs +68 -0
- package/dist/internal/storage-tables.mjs.map +1 -1
- package/dist/internal/storage-validation.d.mts +3 -1
- package/dist/internal/storage-validation.mjs +70 -17
- package/dist/internal/storage-validation.mjs.map +1 -1
- package/dist/internal/totp-native.mjs +1 -1
- package/package.json +1 -1
- package/src/OAuthPersistence.ts +7 -2
- package/src/internal/configuration.ts +72 -15
- package/src/internal/email-native.ts +16 -6
- package/src/internal/email-policy.ts +12 -3
- package/src/internal/models/oauth-model.ts +7 -1
- package/src/internal/models/password-model.ts +2 -1
- package/src/internal/oauth/native-accounts.ts +3 -3
- package/src/internal/oauth/native-registration.ts +17 -6
- package/src/internal/oauth/native-state.ts +38 -18
- package/src/internal/oauth-managed.ts +277 -0
- package/src/internal/passkey-managed.ts +8 -2
- package/src/internal/passkey-native-state.ts +59 -52
- package/src/internal/passkey-native-write.ts +2 -2
- package/src/internal/passkey-native.ts +4 -2
- package/src/internal/passkey-policy.ts +13 -9
- package/src/internal/password-native.ts +45 -11
- package/src/internal/password-policy.ts +16 -8
- package/src/internal/persistence.ts +360 -204
- package/src/internal/phone-native.ts +1 -1
- package/src/internal/registration.ts +53 -40
- package/src/internal/session-native-authority.ts +26 -8
- package/src/internal/session-native-login.ts +9 -4
- package/src/internal/session-native-pending-read.ts +46 -33
- package/src/internal/session-native-record.ts +6 -4
- package/src/internal/session-native-state.ts +32 -10
- package/src/internal/session-native-stateful.ts +147 -52
- package/src/internal/session-native-step-up.ts +31 -24
- package/src/internal/session-native-validity.ts +3 -1
- package/src/internal/sql-authority.ts +5 -1
- package/src/internal/sql-storage-validation.ts +53 -46
- package/src/internal/sql-table.ts +1 -0
- package/src/internal/sqlite-policy-guard.ts +34 -0
- package/src/internal/step-up-state.ts +43 -6
- package/src/internal/storage-mapping.ts +40 -4
- package/src/internal/storage-tables.ts +76 -0
- package/src/internal/storage-validation.ts +127 -27
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { EmailAddressPersistence, EmailUnavailable } from "@yielded/auth/Email";
|
|
2
2
|
import type { LifecycleHooks } from "@yielded/auth/Hooks";
|
|
3
3
|
import { hasCommitScope } from "@yielded/auth/Hooks";
|
|
4
|
+
import {
|
|
5
|
+
OAuthConnectedPersistence,
|
|
6
|
+
OAuthConnectedRevocations,
|
|
7
|
+
OAuthSignInPersistence,
|
|
8
|
+
OAuthUnavailable,
|
|
9
|
+
} from "@yielded/auth/OAuth";
|
|
4
10
|
import {
|
|
5
11
|
PasskeyCredentials,
|
|
6
12
|
PasskeyManagementPersistence,
|
|
@@ -8,7 +14,7 @@ import {
|
|
|
8
14
|
PasskeyUnavailable,
|
|
9
15
|
} from "@yielded/auth/Passkey";
|
|
10
16
|
import { PasswordPersistence, PasswordUnavailable } from "@yielded/auth/Password";
|
|
11
|
-
import { hooksLayer } from "@yielded/auth/Persistence";
|
|
17
|
+
import { hooksLayer, reportAuthDiagnostic } from "@yielded/auth/Persistence";
|
|
12
18
|
import { PhoneSignInTargets, PhoneOtpUnavailable } from "@yielded/auth/PhoneOtp";
|
|
13
19
|
import { ProofPersistence, ProofUnavailable } from "@yielded/auth/Proofs";
|
|
14
20
|
import { AuthenticationAuthority, SessionUnavailable } from "@yielded/auth/Sessions";
|
|
@@ -30,10 +36,12 @@ import {
|
|
|
30
36
|
type SubjectOptions,
|
|
31
37
|
type PasskeyFeature,
|
|
32
38
|
type PasskeyRequirement,
|
|
39
|
+
type OAuthFeature,
|
|
33
40
|
} from "./configuration";
|
|
34
41
|
import { makeNativeEmailAddressServices } from "./email-native";
|
|
35
42
|
import { PersistenceMappingError } from "./mapping-error";
|
|
36
43
|
import type { NativeSqlTables } from "./native-sql-table";
|
|
44
|
+
import { makeManagedOAuth } from "./oauth-managed";
|
|
37
45
|
import { makeManagedPasskeys } from "./passkey-managed";
|
|
38
46
|
import { makeNativePasswordServices } from "./password-native";
|
|
39
47
|
import { makeComposedPhoneTargets } from "./phone-composed";
|
|
@@ -65,6 +73,8 @@ export interface Backend<T extends object, R, Database extends object = object>
|
|
|
65
73
|
readonly describe: (table: T) => StorageTable;
|
|
66
74
|
readonly acquire: Effect.Effect<Database, PersistenceConfigurationError, R | SqlClient.SqlClient>;
|
|
67
75
|
readonly nativeTables: (database: Database) => NativeSqlTables;
|
|
76
|
+
/** Fixed atomic batches replace interactive transactions on backends such as D1. */
|
|
77
|
+
readonly batch?: (database: Database) => NonNullable<SqlBatchCommit["Service"]>;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
const configError = (reason: string) => PersistenceConfigurationError.make({ reason });
|
|
@@ -86,6 +96,11 @@ const timestampKeys = new Set([
|
|
|
86
96
|
"deadline",
|
|
87
97
|
"occurredAt",
|
|
88
98
|
"dedupUntil",
|
|
99
|
+
"binderExpiresAt",
|
|
100
|
+
"refreshClaimedAt",
|
|
101
|
+
"refreshClaimExpiresAt",
|
|
102
|
+
"claimedAt",
|
|
103
|
+
"claimExpiresAt",
|
|
89
104
|
]);
|
|
90
105
|
|
|
91
106
|
export const createPersistence = <T extends object, R, Database extends object = object>(
|
|
@@ -94,27 +109,46 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
94
109
|
const make = <C extends ClaimsCodec, const Id extends string, const A extends Definition<C, Id>>(
|
|
95
110
|
auth: A & Definition<C, Id>,
|
|
96
111
|
) => {
|
|
97
|
-
const features = Object.values(auth.strategies).
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
const features = Object.values(auth.strategies).flatMap((strategy) =>
|
|
113
|
+
strategy.persistence === undefined ? [] : [strategy.persistence],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const phone = features.some((feature) => feature.kind === "phone");
|
|
117
|
+
const password = features.some((feature) => feature.kind === "password");
|
|
100
118
|
|
|
101
119
|
const passkeys = features.filter(
|
|
102
|
-
(feature): feature is PasskeyFeature => feature
|
|
120
|
+
(feature): feature is PasskeyFeature => feature.kind === "passkey",
|
|
103
121
|
);
|
|
104
122
|
|
|
123
|
+
const oauth = features.filter((feature): feature is OAuthFeature => feature?.kind === "oauth");
|
|
124
|
+
const oauthSignIn = oauth.some((feature) => feature.signIn);
|
|
125
|
+
const oauthConnected = oauth.some((feature) => feature.connected !== undefined);
|
|
126
|
+
const stateful = auth.sessionMode === "stateful";
|
|
127
|
+
|
|
128
|
+
const stateless =
|
|
129
|
+
auth.sessionMode === "stateless" &&
|
|
130
|
+
features.length > 0 &&
|
|
131
|
+
features.every((feature) => feature?.kind === "oauth");
|
|
132
|
+
|
|
105
133
|
const management = features.some(
|
|
106
|
-
(feature) => feature
|
|
134
|
+
(feature) => feature.kind === "password" && feature.management,
|
|
107
135
|
);
|
|
108
136
|
|
|
109
|
-
const email = features.some((feature) => feature
|
|
137
|
+
const email = features.some((feature) => feature.kind === "email" && feature.addresses);
|
|
110
138
|
const proofs = phone || management || email;
|
|
111
139
|
|
|
112
|
-
const roles: StorageRole[] = ["identifiers", "credentials"
|
|
140
|
+
const roles: StorageRole[] = ["identifiers", "credentials"];
|
|
113
141
|
|
|
142
|
+
if (stateful) roles.push("sessions");
|
|
143
|
+
roles.push("pending");
|
|
114
144
|
if (password) roles.push("passwords");
|
|
115
145
|
if (email) roles.push("emailCredentials");
|
|
116
146
|
if (passkeys.length > 0) roles.push("passkeyCredentials", "passkeyFlows");
|
|
117
147
|
if (proofs) roles.push("proofs");
|
|
148
|
+
if (oauth.length > 0) roles.push("oauthIdentities", "oauthCredentials");
|
|
149
|
+
if (oauthSignIn) roles.push("oauthSignInFlows");
|
|
150
|
+
if (oauthConnected)
|
|
151
|
+
roles.push("oauthConnectedFlows", "oauthConnectedGrants", "oauthConnectedRevocations");
|
|
118
152
|
|
|
119
153
|
const ConfigKey = Context.Service<
|
|
120
154
|
ConfigId<A["namespace"]>,
|
|
@@ -233,205 +267,229 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
233
267
|
};
|
|
234
268
|
|
|
235
269
|
const services = Layer.effectContext(
|
|
236
|
-
Effect.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
270
|
+
Effect.flatMap(backend.acquire, (database) =>
|
|
271
|
+
Effect.gen(function* () {
|
|
272
|
+
const storage = yield* ConfigKey;
|
|
273
|
+
const client = yield* SqlClient.SqlClient;
|
|
274
|
+
|
|
275
|
+
const dialect = client.onDialectOrElse({
|
|
276
|
+
pg: () => "pg" as const,
|
|
277
|
+
sqlite: () => "sqlite" as const,
|
|
278
|
+
orElse: () => undefined,
|
|
279
|
+
});
|
|
246
280
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
("lifecycle" in feature && feature.lifecycle) ||
|
|
256
|
-
(feature.kind === "email" && !feature.addresses),
|
|
281
|
+
if (dialect === undefined)
|
|
282
|
+
return yield* configError("Use an explicit adapter for this SQL dialect");
|
|
283
|
+
if (
|
|
284
|
+
features.some(
|
|
285
|
+
(feature) =>
|
|
286
|
+
("lifecycle" in feature && feature.lifecycle) ||
|
|
287
|
+
(feature.kind === "email" && !feature.addresses),
|
|
288
|
+
)
|
|
257
289
|
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const validations: StorageValidation[] = [];
|
|
290
|
+
return yield* configError(
|
|
291
|
+
"Use explicit services for strategies without composed persistence support",
|
|
292
|
+
);
|
|
293
|
+
if (storage.namespace !== auth.namespace)
|
|
294
|
+
return yield* configError(
|
|
295
|
+
"Persistence configuration belongs to a different Auth definition",
|
|
296
|
+
);
|
|
297
|
+
const validations: StorageValidation[] = [];
|
|
267
298
|
|
|
268
|
-
|
|
269
|
-
|
|
299
|
+
for (const role of roles) {
|
|
300
|
+
const table = (storage.schema as Partial<Record<StorageRole, T>>)[role];
|
|
270
301
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
302
|
+
if (table === undefined) return yield* configError(`Missing ${role} table`);
|
|
303
|
+
const description = backend.describe(table);
|
|
304
|
+
const columns = description.columns;
|
|
274
305
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
306
|
+
for (const name of Object.keys(storageTables[role].columns)) {
|
|
307
|
+
if (columns[name] === undefined)
|
|
308
|
+
return yield* configError(`Missing ${role}.${name} column`);
|
|
309
|
+
}
|
|
310
|
+
validations.push({ table: description, required: storageTables[role].unique });
|
|
278
311
|
}
|
|
279
|
-
validations.push({
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const mappings = yield* makeMappings(storage);
|
|
287
|
-
|
|
288
|
-
const nativeTables = backend.nativeTables(database);
|
|
289
|
-
const sessionMapping = mappings.sessions(auth.claims, auth.sessions.moduleId);
|
|
290
|
-
const pendingMapping = mappings.pending(auth.claims, auth.sessions.moduleId);
|
|
291
|
-
|
|
292
|
-
const sessionServices = yield* makeNativeStatefulSessionServices(
|
|
293
|
-
nativeTables,
|
|
294
|
-
sessionMapping,
|
|
295
|
-
);
|
|
312
|
+
validations.push({
|
|
313
|
+
table: backend.describe(storage.subjects.table as T),
|
|
314
|
+
required: [[storage.subjects.id]],
|
|
315
|
+
});
|
|
316
|
+
yield* validateStorageBatch(dialect, validations);
|
|
317
|
+
const nativeTables = backend.nativeTables(database);
|
|
318
|
+
const mappings = yield* makeMappings(storage, nativeTables);
|
|
296
319
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
320
|
+
const sessionMapping = stateful
|
|
321
|
+
? mappings.sessions(auth.claims, auth.sessions.moduleId)
|
|
322
|
+
: undefined;
|
|
301
323
|
|
|
302
|
-
|
|
303
|
-
nativeTables,
|
|
304
|
-
pendingMapping,
|
|
305
|
-
);
|
|
324
|
+
const pendingMapping = mappings.pending(auth.claims, auth.sessions.moduleId);
|
|
306
325
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
},
|
|
315
|
-
});
|
|
326
|
+
const { authenticationAuthority } = yield* makeNativeAuthenticationAuthorityServices(
|
|
327
|
+
nativeTables,
|
|
328
|
+
sessionMapping ?? {
|
|
329
|
+
...pendingMapping,
|
|
330
|
+
pending: { pending: pendingMapping.pending, login: pendingMapping.login },
|
|
331
|
+
},
|
|
332
|
+
);
|
|
316
333
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
334
|
+
const { pendingAuthentication } = yield* makeNativePendingAuthenticationServices(
|
|
335
|
+
nativeTables,
|
|
336
|
+
pendingMapping,
|
|
337
|
+
);
|
|
321
338
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
const { sessionStepUpPersistence } = yield* makeNativeSessionStepUpServices(
|
|
340
|
+
nativeTables,
|
|
341
|
+
{
|
|
342
|
+
...pendingMapping,
|
|
343
|
+
source:
|
|
344
|
+
sessionMapping === undefined
|
|
345
|
+
? { kind: "StatelessSigned" }
|
|
346
|
+
: {
|
|
347
|
+
kind: "Stateful",
|
|
348
|
+
session: sessionMapping.session,
|
|
349
|
+
sessionId: sessionMapping.sessionId,
|
|
350
|
+
constraints: { sessionDigest: "unique(session.digest)" },
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
);
|
|
335
354
|
|
|
336
|
-
|
|
355
|
+
const { sessionCleanup } = yield* makeNativeSessionCleanupServices(
|
|
356
|
+
nativeTables,
|
|
357
|
+
pendingMapping,
|
|
358
|
+
);
|
|
337
359
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
360
|
+
let context: Context.Context<never> = Context.make(
|
|
361
|
+
AuthenticationAuthority,
|
|
362
|
+
authenticationAuthority,
|
|
363
|
+
).pipe(
|
|
364
|
+
Context.add(auth.sessions.PendingAuthentication, pendingAuthentication),
|
|
365
|
+
Context.add(auth.sessions.SessionStepUpPersistence, sessionStepUpPersistence),
|
|
366
|
+
Context.add(auth.sessions.SessionCleanup, sessionCleanup),
|
|
342
367
|
);
|
|
343
368
|
|
|
344
|
-
|
|
345
|
-
|
|
369
|
+
if (sessionMapping !== undefined) {
|
|
370
|
+
const sessionServices = yield* makeNativeStatefulSessionServices(
|
|
371
|
+
nativeTables,
|
|
372
|
+
sessionMapping,
|
|
373
|
+
);
|
|
346
374
|
|
|
347
|
-
|
|
348
|
-
|
|
375
|
+
context = context.pipe(
|
|
376
|
+
Context.add(
|
|
377
|
+
auth.sessions.StatefulSessionPersistence,
|
|
378
|
+
sessionServices.statefulSessionPersistence,
|
|
379
|
+
),
|
|
380
|
+
Context.add(auth.sessions.SessionRepository, sessionServices.sessionRepository),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
349
383
|
|
|
350
|
-
const
|
|
351
|
-
backend.nativeTables(database),
|
|
352
|
-
mapping,
|
|
353
|
-
proofMapping,
|
|
354
|
-
);
|
|
384
|
+
const proofMapping = proofs ? mappings.proofs() : undefined;
|
|
355
385
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const key = strategy.RegistrationAuthority;
|
|
386
|
+
if (proofMapping !== undefined) {
|
|
387
|
+
const services = yield* makeNativeProofServices(
|
|
388
|
+
backend.nativeTables(database),
|
|
389
|
+
proofMapping,
|
|
390
|
+
);
|
|
362
391
|
|
|
363
|
-
|
|
364
|
-
|
|
392
|
+
context = Context.add(context, ProofPersistence, services.proofPersistence);
|
|
393
|
+
}
|
|
365
394
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
mappings.passwords(),
|
|
369
|
-
ProvisioningKey,
|
|
370
|
-
name,
|
|
371
|
-
);
|
|
395
|
+
if (password) {
|
|
396
|
+
const mapping = mappings.passwords();
|
|
372
397
|
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
if (email)
|
|
376
|
-
context = Context.add(
|
|
377
|
-
context,
|
|
378
|
-
EmailAddressPersistence,
|
|
379
|
-
(yield* makeNativeEmailAddressServices(
|
|
398
|
+
const services = yield* makeNativePasswordServices(
|
|
380
399
|
backend.nativeTables(database),
|
|
381
|
-
|
|
400
|
+
mapping,
|
|
382
401
|
proofMapping,
|
|
383
|
-
)
|
|
384
|
-
);
|
|
402
|
+
);
|
|
385
403
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
404
|
+
context = Context.add(context, PasswordPersistence, services.passwordPersistence);
|
|
405
|
+
}
|
|
406
|
+
for (const [name, strategy] of Object.entries(auth.strategies)) {
|
|
407
|
+
if (strategy.persistence?.kind !== "password" || !strategy.persistence.management)
|
|
408
|
+
continue;
|
|
409
|
+
const key = strategy.RegistrationAuthority;
|
|
410
|
+
|
|
411
|
+
if (key === undefined)
|
|
412
|
+
return yield* configError(`Missing subject provisioning for ${name}`);
|
|
413
|
+
|
|
414
|
+
const registration = yield* makeRegistrationAuthority(
|
|
391
415
|
backend.nativeTables(database),
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
)
|
|
396
|
-
);
|
|
416
|
+
mappings.passwords(),
|
|
417
|
+
ProvisioningKey,
|
|
418
|
+
name,
|
|
419
|
+
);
|
|
397
420
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
421
|
+
context = Context.add(context, key, registration);
|
|
422
|
+
}
|
|
423
|
+
if (email)
|
|
424
|
+
context = Context.add(
|
|
425
|
+
context,
|
|
426
|
+
EmailAddressPersistence,
|
|
427
|
+
(yield* makeNativeEmailAddressServices(
|
|
428
|
+
backend.nativeTables(database),
|
|
429
|
+
mappings.emails(),
|
|
430
|
+
proofMapping,
|
|
431
|
+
)).emailAddressPersistence,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
if (phone)
|
|
435
|
+
context = Context.add(
|
|
436
|
+
context,
|
|
437
|
+
PhoneSignInTargets,
|
|
438
|
+
yield* makeComposedPhoneTargets(
|
|
439
|
+
backend.nativeTables(database),
|
|
440
|
+
storage,
|
|
441
|
+
mappings.proofs(),
|
|
442
|
+
features.flatMap((feature) => (feature.kind === "phone" ? [feature.moduleId] : [])),
|
|
408
443
|
),
|
|
409
|
-
|
|
410
|
-
backend.nativeTables(database),
|
|
411
|
-
) as Effect.Effect<
|
|
412
|
-
Context.Context<never>,
|
|
413
|
-
PersistenceConfigurationError,
|
|
414
|
-
PasskeyRequirement<A> | Crypto.Crypto | LifecycleHooks | SqlClient.SqlClient
|
|
415
|
-
>;
|
|
416
|
-
|
|
417
|
-
context = Context.merge(context, yield* services);
|
|
418
|
-
}
|
|
444
|
+
);
|
|
419
445
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
446
|
+
if (passkeys.length > 0) {
|
|
447
|
+
// Auth capability metadata determines whether PasskeyConfig is required.
|
|
448
|
+
const services = makeManagedPasskeys(
|
|
449
|
+
{
|
|
450
|
+
storage,
|
|
451
|
+
namespace: auth.namespace,
|
|
452
|
+
dialect,
|
|
453
|
+
features: passkeys,
|
|
454
|
+
passwordModules: features.flatMap((feature) =>
|
|
455
|
+
feature.kind === "password" ? [feature.moduleId] : [],
|
|
456
|
+
),
|
|
457
|
+
},
|
|
458
|
+
backend.nativeTables(database),
|
|
459
|
+
) as Effect.Effect<
|
|
460
|
+
Context.Context<never>,
|
|
461
|
+
PersistenceConfigurationError,
|
|
462
|
+
| PasskeyRequirement<A>
|
|
463
|
+
| Crypto.Crypto
|
|
464
|
+
| LifecycleHooks
|
|
465
|
+
| SqlClient.SqlClient
|
|
466
|
+
| SqlBatchCommit
|
|
467
|
+
>;
|
|
468
|
+
|
|
469
|
+
context = Context.merge(context, yield* services);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (oauth.length > 0)
|
|
473
|
+
context = Context.merge(context, yield* makeManagedOAuth(storage, nativeTables, oauth));
|
|
474
|
+
|
|
475
|
+
// The checked capability metadata above determines exactly these service keys.
|
|
476
|
+
return context as Context.Context<Ports<C, Id, A>>;
|
|
477
|
+
}).pipe(Effect.provideService(SqlBatchCommit, backend.batch?.(database))),
|
|
478
|
+
).pipe(withStorageValidation),
|
|
431
479
|
).pipe(Layer.provide(hooksLayer));
|
|
432
480
|
|
|
433
481
|
const layer = Layer.effectContext(
|
|
434
482
|
Effect.gen(function* () {
|
|
483
|
+
if (!stateful && !stateless) {
|
|
484
|
+
if (auth.sessionMode === "stateless")
|
|
485
|
+
return yield* configError(
|
|
486
|
+
"Composed stateless sessions support only OAuth strategies; use explicit persistence for other strategies",
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
return yield* configError(
|
|
490
|
+
"Composed persistence supports stateful or OAuth-only stateless sessions; state-assisted validity and other session modes require explicit persistence",
|
|
491
|
+
);
|
|
492
|
+
}
|
|
435
493
|
const scope = yield* Effect.scope;
|
|
436
494
|
const captured = yield* Effect.context<Layer.Services<typeof services>>();
|
|
437
495
|
const client = Context.get(captured, SqlClient.SqlClient);
|
|
@@ -455,7 +513,22 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
455
513
|
if (yield* hasCommitScope) return yield* Effect.fail(unavailable());
|
|
456
514
|
yield* requireStandalone(unavailable, client.transactionService);
|
|
457
515
|
}
|
|
458
|
-
|
|
516
|
+
|
|
517
|
+
// Observe the cached failure in this invocation, not the captured setup context.
|
|
518
|
+
const acquired = yield* initialize.pipe(
|
|
519
|
+
Effect.tapError((error) =>
|
|
520
|
+
reportAuthDiagnostic(
|
|
521
|
+
"persistence-initialization",
|
|
522
|
+
Schema.is(PersistenceConfigurationError)(error)
|
|
523
|
+
? "configuration"
|
|
524
|
+
: Schema.is(PersistenceMappingError)(error)
|
|
525
|
+
? "mapping"
|
|
526
|
+
: "unavailable",
|
|
527
|
+
),
|
|
528
|
+
),
|
|
529
|
+
Effect.mapError(unavailable),
|
|
530
|
+
);
|
|
531
|
+
|
|
459
532
|
const value = Context.getOrUndefined(acquired, key);
|
|
460
533
|
|
|
461
534
|
return value === undefined ? yield* Effect.fail(unavailable()) : value;
|
|
@@ -465,18 +538,6 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
465
538
|
const authority = service(AuthenticationAuthority, sessionUnavailable, false);
|
|
466
539
|
const authorityMutation = service(AuthenticationAuthority, sessionUnavailable);
|
|
467
540
|
|
|
468
|
-
const sessions = service(
|
|
469
|
-
auth.sessions.StatefulSessionPersistence,
|
|
470
|
-
sessionUnavailable,
|
|
471
|
-
false,
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
const sessionMutations = service(
|
|
475
|
-
auth.sessions.StatefulSessionPersistence,
|
|
476
|
-
sessionUnavailable,
|
|
477
|
-
);
|
|
478
|
-
|
|
479
|
-
const repository = service(auth.sessions.SessionRepository, sessionUnavailable, false);
|
|
480
541
|
const pendingRead = service(auth.sessions.PendingAuthentication, sessionUnavailable, false);
|
|
481
542
|
const pendingMutation = service(auth.sessions.PendingAuthentication, sessionUnavailable);
|
|
482
543
|
|
|
@@ -496,22 +557,6 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
496
557
|
approve: (input, prepare) =>
|
|
497
558
|
Effect.flatMap(authorityMutation, (s) => s.approve(input, prepare)),
|
|
498
559
|
}).pipe(
|
|
499
|
-
Context.add(auth.sessions.StatefulSessionPersistence, {
|
|
500
|
-
establish: (input, prepare) =>
|
|
501
|
-
Effect.flatMap(sessionMutations, (s) => s.establish(input, prepare)),
|
|
502
|
-
verify: (input) => Effect.flatMap(sessions, (s) => s.verify(input)),
|
|
503
|
-
rotate: (input, prepare) =>
|
|
504
|
-
Effect.flatMap(sessionMutations, (s) => s.rotate(input, prepare)),
|
|
505
|
-
revokeDigest: (digest, prepare) =>
|
|
506
|
-
Effect.flatMap(sessionMutations, (s) => s.revokeDigest(digest, prepare)),
|
|
507
|
-
revoke: (input, prepare) =>
|
|
508
|
-
Effect.flatMap(sessionMutations, (s) => s.revoke(input, prepare)),
|
|
509
|
-
revokeAll: (input, prepare) =>
|
|
510
|
-
Effect.flatMap(sessionMutations, (s) => s.revokeAll(input, prepare)),
|
|
511
|
-
}),
|
|
512
|
-
Context.add(auth.sessions.SessionRepository, {
|
|
513
|
-
list: (input) => Effect.flatMap(repository, (s) => s.list(input)),
|
|
514
|
-
}),
|
|
515
560
|
Context.add(auth.sessions.PendingAuthentication, {
|
|
516
561
|
create: (input, now, prepare) =>
|
|
517
562
|
Effect.flatMap(pendingMutation, (s) => s.create(input, now, prepare)),
|
|
@@ -533,6 +578,40 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
533
578
|
}),
|
|
534
579
|
);
|
|
535
580
|
|
|
581
|
+
if (stateful) {
|
|
582
|
+
const sessions = service(
|
|
583
|
+
auth.sessions.StatefulSessionPersistence,
|
|
584
|
+
sessionUnavailable,
|
|
585
|
+
false,
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const sessionMutations = service(
|
|
589
|
+
auth.sessions.StatefulSessionPersistence,
|
|
590
|
+
sessionUnavailable,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const repository = service(auth.sessions.SessionRepository, sessionUnavailable, false);
|
|
594
|
+
|
|
595
|
+
context = context.pipe(
|
|
596
|
+
Context.add(auth.sessions.StatefulSessionPersistence, {
|
|
597
|
+
establish: (input, prepare) =>
|
|
598
|
+
Effect.flatMap(sessionMutations, (s) => s.establish(input, prepare)),
|
|
599
|
+
verify: (input) => Effect.flatMap(sessions, (s) => s.verify(input)),
|
|
600
|
+
rotate: (input, prepare) =>
|
|
601
|
+
Effect.flatMap(sessionMutations, (s) => s.rotate(input, prepare)),
|
|
602
|
+
revokeDigest: (digest, prepare) =>
|
|
603
|
+
Effect.flatMap(sessionMutations, (s) => s.revokeDigest(digest, prepare)),
|
|
604
|
+
revoke: (input, prepare) =>
|
|
605
|
+
Effect.flatMap(sessionMutations, (s) => s.revoke(input, prepare)),
|
|
606
|
+
revokeAll: (input, prepare) =>
|
|
607
|
+
Effect.flatMap(sessionMutations, (s) => s.revokeAll(input, prepare)),
|
|
608
|
+
}),
|
|
609
|
+
Context.add(auth.sessions.SessionRepository, {
|
|
610
|
+
list: (input) => Effect.flatMap(repository, (s) => s.list(input)),
|
|
611
|
+
}),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
536
615
|
if (proofs) {
|
|
537
616
|
const persistence = service(ProofPersistence, () => ProofUnavailable.make({}));
|
|
538
617
|
|
|
@@ -643,6 +722,83 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
643
722
|
}
|
|
644
723
|
}
|
|
645
724
|
|
|
725
|
+
const oauthUnavailable = () => OAuthUnavailable.make({});
|
|
726
|
+
|
|
727
|
+
function oauthService<I, S>(
|
|
728
|
+
key: Context.Key<I, S>,
|
|
729
|
+
moduleId: string,
|
|
730
|
+
capability: "signIn" | "connected",
|
|
731
|
+
): Effect.Effect<S, OAuthUnavailable> {
|
|
732
|
+
const enabled = oauth.some(
|
|
733
|
+
(feature) =>
|
|
734
|
+
feature.moduleId === moduleId &&
|
|
735
|
+
(capability === "signIn" ? feature.signIn : feature.connected !== undefined),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
return enabled ? service(key, oauthUnavailable) : Effect.fail(oauthUnavailable());
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (oauthSignIn) {
|
|
742
|
+
const persistence = (moduleId: string) =>
|
|
743
|
+
oauthService(OAuthSignInPersistence, moduleId, "signIn");
|
|
744
|
+
|
|
745
|
+
context = Context.add(context, OAuthSignInPersistence, {
|
|
746
|
+
issue: (input, prepare) =>
|
|
747
|
+
Effect.flatMap(persistence(input.context.moduleId), (s) => s.issue(input, prepare)),
|
|
748
|
+
consume: (input, prepare) =>
|
|
749
|
+
Effect.flatMap(persistence(input.moduleId), (s) => s.consume(input, prepare)),
|
|
750
|
+
resolve: (input) =>
|
|
751
|
+
Effect.flatMap(persistence(input.moduleId), (s) => s.resolve(input)),
|
|
752
|
+
cleanup: (input, prepare) =>
|
|
753
|
+
Effect.flatMap(persistence(input.moduleId), (s) => s.cleanup(input, prepare)),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (oauthConnected) {
|
|
757
|
+
const persistence = (moduleId: string) =>
|
|
758
|
+
oauthService(OAuthConnectedPersistence, moduleId, "connected");
|
|
759
|
+
|
|
760
|
+
const revocations = (moduleId: string) =>
|
|
761
|
+
oauthService(OAuthConnectedRevocations, moduleId, "connected");
|
|
762
|
+
|
|
763
|
+
context = context.pipe(
|
|
764
|
+
Context.add(OAuthConnectedPersistence, {
|
|
765
|
+
read: (input) => Effect.flatMap(persistence(input.moduleId), (s) => s.read(input)),
|
|
766
|
+
issue: (input, prepare) =>
|
|
767
|
+
Effect.flatMap(persistence(input.context.moduleId), (s) => s.issue(input, prepare)),
|
|
768
|
+
consume: (input, prepare) =>
|
|
769
|
+
Effect.flatMap(persistence(input.moduleId), (s) => s.consume(input, prepare)),
|
|
770
|
+
settle: (input, prepare) =>
|
|
771
|
+
Effect.flatMap(persistence(input.grant.context.moduleId), (s) =>
|
|
772
|
+
s.settle(input, prepare),
|
|
773
|
+
),
|
|
774
|
+
list: (input) =>
|
|
775
|
+
Effect.flatMap(persistence(input.authorization.moduleId), (s) => s.list(input)),
|
|
776
|
+
disconnect: (input, prepare) =>
|
|
777
|
+
Effect.flatMap(persistence(input.key.moduleId), (s) =>
|
|
778
|
+
s.disconnect(input, prepare),
|
|
779
|
+
),
|
|
780
|
+
claimRefresh: (input, prepare) =>
|
|
781
|
+
Effect.flatMap(persistence(input.grant.context.moduleId), (s) =>
|
|
782
|
+
s.claimRefresh(input, prepare),
|
|
783
|
+
),
|
|
784
|
+
settleRefresh: (input, prepare) =>
|
|
785
|
+
Effect.flatMap(persistence(input.claim.grant.context.moduleId), (s) =>
|
|
786
|
+
s.settleRefresh(input, prepare),
|
|
787
|
+
),
|
|
788
|
+
cleanup: (input, prepare) =>
|
|
789
|
+
Effect.flatMap(persistence(input.moduleId), (s) => s.cleanup(input, prepare)),
|
|
790
|
+
}),
|
|
791
|
+
Context.add(OAuthConnectedRevocations, {
|
|
792
|
+
claim: (input, prepare) =>
|
|
793
|
+
Effect.flatMap(revocations(input.moduleId), (s) => s.claim(input, prepare)),
|
|
794
|
+
settle: (input, prepare) =>
|
|
795
|
+
Effect.flatMap(revocations(input.claim.job.grant.context.moduleId), (s) =>
|
|
796
|
+
s.settle(input, prepare),
|
|
797
|
+
),
|
|
798
|
+
}),
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
646
802
|
// Match the same capability-selected service keys as the acquired graph.
|
|
647
803
|
return context as Context.Context<Ports<C, Id, A>>;
|
|
648
804
|
}),
|