@yielded/auth-persistence 0.1.0-beta.31 → 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 +9 -2
- 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 +31 -5
- 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/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 +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 +9 -6
- 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/persistence.mjs +96 -37
- 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/session-native-authority.mjs +6 -5
- 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 +2 -1
- 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 +3 -0
- 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 +65 -37
- 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 +14 -18
- 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/step-up-state.mjs +19 -6
- package/dist/internal/step-up-state.mjs.map +1 -1
- package/dist/internal/storage-mapping.mjs +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 +46 -7
- 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/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-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 +17 -9
- package/src/internal/password-policy.ts +16 -8
- package/src/internal/persistence.ts +216 -70
- package/src/internal/phone-native.ts +1 -1
- package/src/internal/session-native-authority.ts +23 -7
- package/src/internal/session-native-login.ts +6 -3
- 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 +3 -0
- package/src/internal/session-native-stateful.ts +144 -51
- package/src/internal/session-native-step-up.ts +27 -23
- 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/step-up-state.ts +43 -6
- 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";
|
|
@@ -88,6 +96,11 @@ const timestampKeys = new Set([
|
|
|
88
96
|
"deadline",
|
|
89
97
|
"occurredAt",
|
|
90
98
|
"dedupUntil",
|
|
99
|
+
"binderExpiresAt",
|
|
100
|
+
"refreshClaimedAt",
|
|
101
|
+
"refreshClaimExpiresAt",
|
|
102
|
+
"claimedAt",
|
|
103
|
+
"claimExpiresAt",
|
|
91
104
|
]);
|
|
92
105
|
|
|
93
106
|
export const createPersistence = <T extends object, R, Database extends object = object>(
|
|
@@ -96,27 +109,46 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
96
109
|
const make = <C extends ClaimsCodec, const Id extends string, const A extends Definition<C, Id>>(
|
|
97
110
|
auth: A & Definition<C, Id>,
|
|
98
111
|
) => {
|
|
99
|
-
const features = Object.values(auth.strategies).
|
|
100
|
-
|
|
101
|
-
|
|
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");
|
|
102
118
|
|
|
103
119
|
const passkeys = features.filter(
|
|
104
|
-
(feature): feature is PasskeyFeature => feature
|
|
120
|
+
(feature): feature is PasskeyFeature => feature.kind === "passkey",
|
|
105
121
|
);
|
|
106
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
|
+
|
|
107
133
|
const management = features.some(
|
|
108
|
-
(feature) => feature
|
|
134
|
+
(feature) => feature.kind === "password" && feature.management,
|
|
109
135
|
);
|
|
110
136
|
|
|
111
|
-
const email = features.some((feature) => feature
|
|
137
|
+
const email = features.some((feature) => feature.kind === "email" && feature.addresses);
|
|
112
138
|
const proofs = phone || management || email;
|
|
113
139
|
|
|
114
|
-
const roles: StorageRole[] = ["identifiers", "credentials"
|
|
140
|
+
const roles: StorageRole[] = ["identifiers", "credentials"];
|
|
115
141
|
|
|
142
|
+
if (stateful) roles.push("sessions");
|
|
143
|
+
roles.push("pending");
|
|
116
144
|
if (password) roles.push("passwords");
|
|
117
145
|
if (email) roles.push("emailCredentials");
|
|
118
146
|
if (passkeys.length > 0) roles.push("passkeyCredentials", "passkeyFlows");
|
|
119
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");
|
|
120
152
|
|
|
121
153
|
const ConfigKey = Context.Service<
|
|
122
154
|
ConfigId<A["namespace"]>,
|
|
@@ -248,12 +280,9 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
248
280
|
|
|
249
281
|
if (dialect === undefined)
|
|
250
282
|
return yield* configError("Use an explicit adapter for this SQL dialect");
|
|
251
|
-
if (auth.sessionMode !== "stateful")
|
|
252
|
-
return yield* configError("The composed layer currently requires stateful sessions");
|
|
253
283
|
if (
|
|
254
284
|
features.some(
|
|
255
285
|
(feature) =>
|
|
256
|
-
feature === undefined ||
|
|
257
286
|
("lifecycle" in feature && feature.lifecycle) ||
|
|
258
287
|
(feature.kind === "email" && !feature.addresses),
|
|
259
288
|
)
|
|
@@ -287,17 +316,19 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
287
316
|
yield* validateStorageBatch(dialect, validations);
|
|
288
317
|
const nativeTables = backend.nativeTables(database);
|
|
289
318
|
const mappings = yield* makeMappings(storage, nativeTables);
|
|
290
|
-
const sessionMapping = mappings.sessions(auth.claims, auth.sessions.moduleId);
|
|
291
|
-
const pendingMapping = mappings.pending(auth.claims, auth.sessions.moduleId);
|
|
292
319
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
320
|
+
const sessionMapping = stateful
|
|
321
|
+
? mappings.sessions(auth.claims, auth.sessions.moduleId)
|
|
322
|
+
: undefined;
|
|
323
|
+
|
|
324
|
+
const pendingMapping = mappings.pending(auth.claims, auth.sessions.moduleId);
|
|
297
325
|
|
|
298
326
|
const { authenticationAuthority } = yield* makeNativeAuthenticationAuthorityServices(
|
|
299
327
|
nativeTables,
|
|
300
|
-
sessionMapping
|
|
328
|
+
sessionMapping ?? {
|
|
329
|
+
...pendingMapping,
|
|
330
|
+
pending: { pending: pendingMapping.pending, login: pendingMapping.login },
|
|
331
|
+
},
|
|
301
332
|
);
|
|
302
333
|
|
|
303
334
|
const { pendingAuthentication } = yield* makeNativePendingAuthenticationServices(
|
|
@@ -309,12 +340,15 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
309
340
|
nativeTables,
|
|
310
341
|
{
|
|
311
342
|
...pendingMapping,
|
|
312
|
-
source:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
},
|
|
318
352
|
},
|
|
319
353
|
);
|
|
320
354
|
|
|
@@ -327,16 +361,26 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
327
361
|
AuthenticationAuthority,
|
|
328
362
|
authenticationAuthority,
|
|
329
363
|
).pipe(
|
|
330
|
-
Context.add(
|
|
331
|
-
auth.sessions.StatefulSessionPersistence,
|
|
332
|
-
sessionServices.statefulSessionPersistence,
|
|
333
|
-
),
|
|
334
|
-
Context.add(auth.sessions.SessionRepository, sessionServices.sessionRepository),
|
|
335
364
|
Context.add(auth.sessions.PendingAuthentication, pendingAuthentication),
|
|
336
365
|
Context.add(auth.sessions.SessionStepUpPersistence, sessionStepUpPersistence),
|
|
337
366
|
Context.add(auth.sessions.SessionCleanup, sessionCleanup),
|
|
338
367
|
);
|
|
339
368
|
|
|
369
|
+
if (sessionMapping !== undefined) {
|
|
370
|
+
const sessionServices = yield* makeNativeStatefulSessionServices(
|
|
371
|
+
nativeTables,
|
|
372
|
+
sessionMapping,
|
|
373
|
+
);
|
|
374
|
+
|
|
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
|
+
}
|
|
383
|
+
|
|
340
384
|
const proofMapping = proofs ? mappings.proofs() : undefined;
|
|
341
385
|
|
|
342
386
|
if (proofMapping !== undefined) {
|
|
@@ -395,9 +439,7 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
395
439
|
backend.nativeTables(database),
|
|
396
440
|
storage,
|
|
397
441
|
mappings.proofs(),
|
|
398
|
-
features.flatMap((feature) =>
|
|
399
|
-
feature?.kind === "phone" ? [feature.moduleId] : [],
|
|
400
|
-
),
|
|
442
|
+
features.flatMap((feature) => (feature.kind === "phone" ? [feature.moduleId] : [])),
|
|
401
443
|
),
|
|
402
444
|
);
|
|
403
445
|
|
|
@@ -410,7 +452,7 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
410
452
|
dialect,
|
|
411
453
|
features: passkeys,
|
|
412
454
|
passwordModules: features.flatMap((feature) =>
|
|
413
|
-
feature
|
|
455
|
+
feature.kind === "password" ? [feature.moduleId] : [],
|
|
414
456
|
),
|
|
415
457
|
},
|
|
416
458
|
backend.nativeTables(database),
|
|
@@ -427,21 +469,27 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
427
469
|
context = Context.merge(context, yield* services);
|
|
428
470
|
}
|
|
429
471
|
|
|
472
|
+
if (oauth.length > 0)
|
|
473
|
+
context = Context.merge(context, yield* makeManagedOAuth(storage, nativeTables, oauth));
|
|
474
|
+
|
|
430
475
|
// The checked capability metadata above determines exactly these service keys.
|
|
431
476
|
return context as Context.Context<Ports<C, Id, A>>;
|
|
432
477
|
}).pipe(Effect.provideService(SqlBatchCommit, backend.batch?.(database))),
|
|
433
|
-
).pipe(
|
|
434
|
-
withStorageValidation,
|
|
435
|
-
Effect.mapError((error) =>
|
|
436
|
-
Schema.is(PersistenceConfigurationError)(error)
|
|
437
|
-
? error
|
|
438
|
-
: configError("Cannot acquire SQL persistence"),
|
|
439
|
-
),
|
|
440
|
-
),
|
|
478
|
+
).pipe(withStorageValidation),
|
|
441
479
|
).pipe(Layer.provide(hooksLayer));
|
|
442
480
|
|
|
443
481
|
const layer = Layer.effectContext(
|
|
444
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
|
+
}
|
|
445
493
|
const scope = yield* Effect.scope;
|
|
446
494
|
const captured = yield* Effect.context<Layer.Services<typeof services>>();
|
|
447
495
|
const client = Context.get(captured, SqlClient.SqlClient);
|
|
@@ -465,7 +513,22 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
465
513
|
if (yield* hasCommitScope) return yield* Effect.fail(unavailable());
|
|
466
514
|
yield* requireStandalone(unavailable, client.transactionService);
|
|
467
515
|
}
|
|
468
|
-
|
|
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
|
+
|
|
469
532
|
const value = Context.getOrUndefined(acquired, key);
|
|
470
533
|
|
|
471
534
|
return value === undefined ? yield* Effect.fail(unavailable()) : value;
|
|
@@ -475,18 +538,6 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
475
538
|
const authority = service(AuthenticationAuthority, sessionUnavailable, false);
|
|
476
539
|
const authorityMutation = service(AuthenticationAuthority, sessionUnavailable);
|
|
477
540
|
|
|
478
|
-
const sessions = service(
|
|
479
|
-
auth.sessions.StatefulSessionPersistence,
|
|
480
|
-
sessionUnavailable,
|
|
481
|
-
false,
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
const sessionMutations = service(
|
|
485
|
-
auth.sessions.StatefulSessionPersistence,
|
|
486
|
-
sessionUnavailable,
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
const repository = service(auth.sessions.SessionRepository, sessionUnavailable, false);
|
|
490
541
|
const pendingRead = service(auth.sessions.PendingAuthentication, sessionUnavailable, false);
|
|
491
542
|
const pendingMutation = service(auth.sessions.PendingAuthentication, sessionUnavailable);
|
|
492
543
|
|
|
@@ -506,22 +557,6 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
506
557
|
approve: (input, prepare) =>
|
|
507
558
|
Effect.flatMap(authorityMutation, (s) => s.approve(input, prepare)),
|
|
508
559
|
}).pipe(
|
|
509
|
-
Context.add(auth.sessions.StatefulSessionPersistence, {
|
|
510
|
-
establish: (input, prepare) =>
|
|
511
|
-
Effect.flatMap(sessionMutations, (s) => s.establish(input, prepare)),
|
|
512
|
-
verify: (input) => Effect.flatMap(sessions, (s) => s.verify(input)),
|
|
513
|
-
rotate: (input, prepare) =>
|
|
514
|
-
Effect.flatMap(sessionMutations, (s) => s.rotate(input, prepare)),
|
|
515
|
-
revokeDigest: (digest, prepare) =>
|
|
516
|
-
Effect.flatMap(sessionMutations, (s) => s.revokeDigest(digest, prepare)),
|
|
517
|
-
revoke: (input, prepare) =>
|
|
518
|
-
Effect.flatMap(sessionMutations, (s) => s.revoke(input, prepare)),
|
|
519
|
-
revokeAll: (input, prepare) =>
|
|
520
|
-
Effect.flatMap(sessionMutations, (s) => s.revokeAll(input, prepare)),
|
|
521
|
-
}),
|
|
522
|
-
Context.add(auth.sessions.SessionRepository, {
|
|
523
|
-
list: (input) => Effect.flatMap(repository, (s) => s.list(input)),
|
|
524
|
-
}),
|
|
525
560
|
Context.add(auth.sessions.PendingAuthentication, {
|
|
526
561
|
create: (input, now, prepare) =>
|
|
527
562
|
Effect.flatMap(pendingMutation, (s) => s.create(input, now, prepare)),
|
|
@@ -543,6 +578,40 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
543
578
|
}),
|
|
544
579
|
);
|
|
545
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
|
+
|
|
546
615
|
if (proofs) {
|
|
547
616
|
const persistence = service(ProofPersistence, () => ProofUnavailable.make({}));
|
|
548
617
|
|
|
@@ -653,6 +722,83 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
653
722
|
}
|
|
654
723
|
}
|
|
655
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
|
+
|
|
656
802
|
// Match the same capability-selected service keys as the acquired graph.
|
|
657
803
|
return context as Context.Context<Ports<C, Id, A>>;
|
|
658
804
|
}),
|
|
@@ -428,7 +428,7 @@ export const makeNativePhoneServices = Effect.fnUntraced(function* (
|
|
|
428
428
|
const requirement =
|
|
429
429
|
yield* Schema.decodeEffect(AuthenticationRequirement)(originalRequirement);
|
|
430
430
|
|
|
431
|
-
if (!passwordEvidenceSatisfiedAt(evidence, requirement, captured.now))
|
|
431
|
+
if (!(yield* passwordEvidenceSatisfiedAt(evidence, requirement, captured.now)))
|
|
432
432
|
return yield* rejected();
|
|
433
433
|
}
|
|
434
434
|
if (evidence.proofs.length === 0) return yield* rejected();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LifecycleHooks } from "@yielded/auth/Hooks";
|
|
2
|
+
import { AuthenticationClock } from "@yielded/auth/Operations";
|
|
2
3
|
import {
|
|
3
4
|
type AuthenticationAuthority,
|
|
4
5
|
StaleAuthentication,
|
|
@@ -51,11 +52,14 @@ export const sessionEvidenceDeadline = Effect.fnUntraced(function* (
|
|
|
51
52
|
if (!assessed.satisfied) return yield* StaleAuthentication.make({});
|
|
52
53
|
|
|
53
54
|
const boundaries = [
|
|
54
|
-
...new Set(
|
|
55
|
-
evidence.proofs.map(
|
|
55
|
+
...new Set([
|
|
56
|
+
...evidence.proofs.map(
|
|
56
57
|
(proof) => DateTime.toEpochMillis(proof.verifiedAt) + requirement.maximumAgeMillis,
|
|
57
58
|
),
|
|
58
|
-
|
|
59
|
+
...(evidence.completionExpiresAt === undefined
|
|
60
|
+
? []
|
|
61
|
+
: [DateTime.toEpochMillis(evidence.completionExpiresAt)]),
|
|
62
|
+
]),
|
|
59
63
|
]
|
|
60
64
|
.filter((n) => n > now)
|
|
61
65
|
.sort((a, b) => a - b);
|
|
@@ -139,11 +143,12 @@ export const makeNativeAuthenticationAuthorityServices = Effect.fnUntraced(funct
|
|
|
139
143
|
});
|
|
140
144
|
|
|
141
145
|
const native = <A, E, R>(work: Effect.Effect<A, E, R>) =>
|
|
142
|
-
batch === undefined
|
|
146
|
+
(batch === undefined
|
|
143
147
|
? executor.operation(normalizeSessionOperation(work))
|
|
144
148
|
: executor
|
|
145
149
|
.operationBatch(normalizeSessionOperation(work))
|
|
146
|
-
.pipe(Effect.provideService(SqlBatchCommit, batch))
|
|
150
|
+
.pipe(Effect.provideService(SqlBatchCommit, batch))
|
|
151
|
+
).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy));
|
|
147
152
|
|
|
148
153
|
const authenticationAuthority: AuthenticationAuthority["Service"] = {
|
|
149
154
|
capture: (subjectId, credentialIds) =>
|
|
@@ -183,10 +188,20 @@ export const makeNativeAuthenticationAuthorityServices = Effect.fnUntraced(funct
|
|
|
183
188
|
current.now,
|
|
184
189
|
);
|
|
185
190
|
|
|
186
|
-
const
|
|
191
|
+
const issued = DateTime.toEpochMillis(input.issuedAt),
|
|
192
|
+
expires = DateTime.toEpochMillis(input.expiresAt),
|
|
187
193
|
absolute = DateTime.toEpochMillis(input.absoluteExpiresAt);
|
|
188
194
|
|
|
189
|
-
if (
|
|
195
|
+
if (
|
|
196
|
+
Math.max(issued, DateTime.toEpochMillis(input.now)) - current.now >
|
|
197
|
+
state.clockPolicy.futureToleranceMillis ||
|
|
198
|
+
input.evidence.proofs.some(
|
|
199
|
+
(proof) => DateTime.toEpochMillis(proof.verifiedAt) > issued,
|
|
200
|
+
) ||
|
|
201
|
+
issued >= expires ||
|
|
202
|
+
current.now >= expires ||
|
|
203
|
+
expires > absolute
|
|
204
|
+
)
|
|
190
205
|
return yield* StaleAuthentication.make({});
|
|
191
206
|
|
|
192
207
|
const pendingExpiry =
|
|
@@ -226,6 +241,7 @@ export const makeNativeAuthenticationAuthorityServices = Effect.fnUntraced(funct
|
|
|
226
241
|
if (rows.length !== 1) return yield* PendingAuthenticationInvalid.make({});
|
|
227
242
|
}
|
|
228
243
|
}).pipe(
|
|
244
|
+
Effect.provideService(AuthenticationClock, state.clockPolicy),
|
|
229
245
|
Effect.mapError((cause) =>
|
|
230
246
|
PersistenceMappingError.make({ operation: "decode", cause }),
|
|
231
247
|
),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { LifecycleHooks } from "@yielded/auth/Hooks";
|
|
2
|
+
import { AuthenticationClock } from "@yielded/auth/Operations";
|
|
2
3
|
import {
|
|
3
4
|
PendingAuthenticationInvalid,
|
|
4
5
|
StaleAuthentication,
|
|
5
6
|
SessionConflict,
|
|
6
7
|
type PendingAuthentication,
|
|
7
8
|
type PendingAuthenticationRecord,
|
|
9
|
+
type SessionUnavailable,
|
|
8
10
|
} from "@yielded/auth/Sessions";
|
|
9
11
|
import { DateTime, Effect, Option } from "effect";
|
|
10
12
|
import type { SqlClient } from "effect/sql/SqlClient";
|
|
@@ -46,7 +48,7 @@ export const makeNativePendingAuthenticationServices = Effect.fnUntraced(functio
|
|
|
46
48
|
mapping: NativePendingAuthenticationMapping<Claims>,
|
|
47
49
|
): Effect.fn.Return<
|
|
48
50
|
{ readonly pendingAuthentication: PendingAuthentication<Claims> },
|
|
49
|
-
|
|
51
|
+
SessionUnavailable,
|
|
50
52
|
SqlClient | LifecycleHooks | SqlBatchCommit
|
|
51
53
|
> {
|
|
52
54
|
const batch = yield* SqlBatchCommit;
|
|
@@ -67,11 +69,12 @@ export const makeNativePendingAuthenticationServices = Effect.fnUntraced(functio
|
|
|
67
69
|
work: Effect.Effect<A, E, R>,
|
|
68
70
|
mode: "transaction" | "statement" = "transaction",
|
|
69
71
|
) =>
|
|
70
|
-
batch === undefined
|
|
72
|
+
(batch === undefined
|
|
71
73
|
? executor.operation(normalizeSessionOperation(work), mode)
|
|
72
74
|
: executor
|
|
73
75
|
.operationBatch(normalizeSessionOperation(work))
|
|
74
|
-
.pipe(Effect.provideService(SqlBatchCommit, batch))
|
|
76
|
+
.pipe(Effect.provideService(SqlBatchCommit, batch))
|
|
77
|
+
).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy));
|
|
75
78
|
|
|
76
79
|
const verifyPayload = Effect.fnUntraced(function* (
|
|
77
80
|
stored: import("./session-native-pending").StoredSessionPending,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TokenDigest } from "@yielded/auth/Schema";
|
|
1
|
+
import type { SubjectId, TokenDigest } from "@yielded/auth/Schema";
|
|
2
2
|
import type { PendingAuthenticationKind } from "@yielded/auth/Sessions";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
4
|
|
|
@@ -6,13 +6,15 @@ import type { NativeSqlTables } from "./native-sql-table";
|
|
|
6
6
|
import { makeNativeSessionPending } from "./session-native-pending";
|
|
7
7
|
import {
|
|
8
8
|
makeNativeSessionAuthorityState,
|
|
9
|
+
sessionInvariant,
|
|
10
|
+
sessionUnavailable,
|
|
9
11
|
type NativeSessionAuthorityMapping,
|
|
10
12
|
} from "./session-native-state";
|
|
11
13
|
import { exactSqlText } from "./sql-change";
|
|
14
|
+
import { canJoinTextColumns } from "./storage-validation";
|
|
12
15
|
|
|
13
|
-
/** Both
|
|
14
|
-
*
|
|
15
|
-
* validate each decoded owner before returning current policy. */
|
|
16
|
+
/** Both ports re-read the pending row with current authority in one snapshot.
|
|
17
|
+
* Discovery only selects the independently bound owner for incompatible IDs. */
|
|
16
18
|
export const makeNativeSessionPendingReader = Effect.fnUntraced(function* (
|
|
17
19
|
tables: NativeSqlTables,
|
|
18
20
|
mapping: NativeSessionAuthorityMapping & Parameters<typeof makeNativeSessionPending>[1],
|
|
@@ -21,47 +23,58 @@ export const makeNativeSessionPendingReader = Effect.fnUntraced(function* (
|
|
|
21
23
|
const state = yield* makeNativeSessionAuthorityState(tables, mapping, batch);
|
|
22
24
|
const pending = yield* makeNativeSessionPending(tables, mapping, batch);
|
|
23
25
|
|
|
26
|
+
const joinedIds = yield* canJoinTextColumns([
|
|
27
|
+
pending.table.unencodedTextColumn?.(pending.p.subjectId),
|
|
28
|
+
state.subject.unencodedTextColumn?.(mapping.subject.id),
|
|
29
|
+
state.credential.unencodedTextColumn?.(mapping.credential.subjectId),
|
|
30
|
+
]).pipe(Effect.mapError(sessionUnavailable));
|
|
31
|
+
|
|
24
32
|
const read = Effect.fnUntraced(function* (kind: PendingAuthenticationKind, digest: TokenDigest) {
|
|
25
33
|
const { sql, now } = state,
|
|
26
34
|
{ p, table } = pending;
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
state.subject.unencodedTextColumn?.(mapping.subject.id) !== undefined &&
|
|
31
|
-
state.credential.unencodedTextColumn?.(mapping.credential.subjectId) !== undefined;
|
|
36
|
+
let discoveredSubjectId: SubjectId | undefined;
|
|
37
|
+
let nativeId: unknown;
|
|
32
38
|
|
|
33
|
-
if (
|
|
34
|
-
const
|
|
35
|
-
st = state.joinedSubject,
|
|
36
|
-
ct = state.joinedCredential,
|
|
37
|
-
s = mapping.subject,
|
|
38
|
-
c = mapping.credential;
|
|
39
|
+
if (!joinedIds) {
|
|
40
|
+
const selected = yield* pending.read(kind, digest);
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
if (selected === undefined) return undefined;
|
|
43
|
+
discoveredSubjectId = selected.record.subjectId;
|
|
44
|
+
nativeId = yield* mapping.subjectId.toNative(discoveredSubjectId);
|
|
45
|
+
}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
const pt = table.as("authentication_pending"),
|
|
48
|
+
st = state.joinedSubject,
|
|
49
|
+
ct = state.joinedCredential,
|
|
50
|
+
s = mapping.subject,
|
|
51
|
+
c = mapping.credential;
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
rows.map((row) => ct.decode(row, "c_")),
|
|
52
|
-
rows[0].engine_now,
|
|
53
|
-
);
|
|
53
|
+
const bindOwner = (table: typeof pt, key: string) =>
|
|
54
|
+
joinedIds
|
|
55
|
+
? exactSqlText(sql, table.column(key), pt.column(p.subjectId))
|
|
56
|
+
: state.id(table, key, nativeId);
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const rows =
|
|
59
|
+
yield* sql`select ${pt.fields("p_")}, ${st.fields("s_")}, ${ct.fields("c_")}, ${now} as engine_now from ${pt.name} join ${st.name} on ${bindOwner(st, s.id)} and ${state.activeSubject(st)} left join ${ct.name} on ${bindOwner(ct, c.subjectId)} and ${state.activeCredential(ct)} where ${pending.predicate(pt, kind, digest)}${joinedIds ? sql`` : sql` and ${bindOwner(pt, p.subjectId)}`} limit 65`;
|
|
60
|
+
|
|
61
|
+
if (rows[0] === undefined) return undefined;
|
|
62
|
+
const record = yield* pending.decode(kind, pt.decode(rows[0], "p_"));
|
|
63
|
+
|
|
64
|
+
sessionInvariant(joinedIds || record.subjectId === discoveredSubjectId);
|
|
65
|
+
const owner = yield* mapping.subjectId.toNative(record.subjectId);
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
const authority = yield* state.decode(
|
|
68
|
+
record.subjectId,
|
|
69
|
+
owner,
|
|
70
|
+
rows[0],
|
|
71
|
+
rows.map((row) => ct.decode(row, "c_")),
|
|
72
|
+
rows[0].engine_now,
|
|
73
|
+
);
|
|
61
74
|
|
|
62
|
-
return authority === undefined || authority.now >=
|
|
75
|
+
return authority === undefined || authority.now >= record.expiresAtMillis
|
|
63
76
|
? undefined
|
|
64
|
-
: { record
|
|
77
|
+
: { record, authority };
|
|
65
78
|
});
|
|
66
79
|
|
|
67
80
|
return { state, pending, read };
|