@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
|
@@ -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,12 +1,10 @@
|
|
|
1
1
|
import type { LifecycleHooks } from "@yielded/auth/Hooks";
|
|
2
|
-
import type { LoginIdentifier } from "@yielded/auth/Identity";
|
|
3
2
|
import { PasswordUnavailable } from "@yielded/auth/Password";
|
|
4
|
-
import type { SubjectId } from "@yielded/auth/Schema";
|
|
5
3
|
import { SecurityRevision } from "@yielded/auth/Sessions";
|
|
6
4
|
import { type Context, Crypto, Effect, Schema, Option, Cause } from "effect";
|
|
7
5
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
8
6
|
|
|
9
|
-
import { PersistenceConfigurationError } from "./configuration";
|
|
7
|
+
import { PersistenceConfigurationError, type SubjectProvisioning } from "./configuration";
|
|
10
8
|
import { randomId } from "./crypto";
|
|
11
9
|
import type { AnyPasswordPersistenceMapping } from "./models/password-model";
|
|
12
10
|
import type { NativeSqlTables } from "./native-sql-table";
|
|
@@ -15,13 +13,9 @@ import {
|
|
|
15
13
|
PasswordIdentifierTaken,
|
|
16
14
|
} from "./password-registration-native";
|
|
17
15
|
import type { PasswordRegistrationAuthority } from "./registration-contract";
|
|
18
|
-
import { makeSqlCommitExecutor, CurrentSqlCommit } from "./sql-commit";
|
|
16
|
+
import { makeSqlCommitExecutor, CurrentSqlCommit, SqlBatchCommit } from "./sql-commit";
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
readonly requestId: string;
|
|
22
|
-
readonly identifier: LoginIdentifier;
|
|
23
|
-
readonly registration: unknown;
|
|
24
|
-
}) => Effect.Effect<SubjectId, PasswordUnavailable>;
|
|
18
|
+
const SubjectValues = Schema.Record(Schema.String, Schema.Unknown);
|
|
25
19
|
|
|
26
20
|
/** Application provisioning and the unverified identifier/password binding share
|
|
27
21
|
* one physical owner. Identifier uniqueness arbitrates registration; correlation
|
|
@@ -34,49 +28,56 @@ export const makeRegistrationAuthority = Effect.fnUntraced(function* <R>(
|
|
|
34
28
|
): Effect.fn.Return<
|
|
35
29
|
PasswordRegistrationAuthority<unknown>,
|
|
36
30
|
PersistenceConfigurationError,
|
|
37
|
-
LifecycleHooks | SqlClient | Crypto.Crypto | R
|
|
31
|
+
LifecycleHooks | SqlClient | Crypto.Crypto | SqlBatchCommit | R
|
|
38
32
|
> {
|
|
39
33
|
const sql = (yield* SqlClient).withoutTransforms();
|
|
40
34
|
const crypto = yield* Crypto.Crypto;
|
|
41
35
|
const executor = yield* makeSqlCommitExecutor(() => PasswordUnavailable.make({}));
|
|
42
36
|
const parent = yield* Effect.serviceOption(CurrentSqlCommit);
|
|
43
|
-
const
|
|
37
|
+
const batch = yield* SqlBatchCommit;
|
|
38
|
+
const store = yield* makeNativePasswordRegistrationStore(tables, mapping, batch !== undefined);
|
|
44
39
|
// The strategy has already decoded its registration Schema. Only its callback
|
|
45
40
|
// signature is erased by the heterogeneous strategy service map.
|
|
46
|
-
const creators = (yield* provisioning) as Readonly<Record<string,
|
|
41
|
+
const creators = (yield* provisioning) as Readonly<Record<string, SubjectProvisioning<unknown>>>;
|
|
47
42
|
const createSubject = creators[strategy];
|
|
48
43
|
|
|
49
44
|
if (createSubject === undefined)
|
|
50
45
|
return yield* PersistenceConfigurationError.make({
|
|
51
46
|
reason: `Missing subject provisioning for ${strategy}`,
|
|
52
47
|
});
|
|
48
|
+
if (batch !== undefined && typeof createSubject === "function")
|
|
49
|
+
return yield* PersistenceConfigurationError.make({
|
|
50
|
+
reason: `D1 subject provisioning for ${strategy} requires { values }; return the new subject row instead of inserting it`,
|
|
51
|
+
});
|
|
53
52
|
const s = mapping.subject;
|
|
54
53
|
const subject = tables(s.table);
|
|
55
54
|
const lock = sql.onDialectOrElse({ sqlite: () => sql``, orElse: () => sql`for update` });
|
|
56
55
|
|
|
57
56
|
const run = <A, E, R2>(work: Effect.Effect<A, E, R2>, suppressed: Effect.Effect<A, E, R2>) =>
|
|
58
|
-
|
|
59
|
-
? executor.
|
|
60
|
-
:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
batch !== undefined
|
|
58
|
+
? executor.batch(work).pipe(Effect.provideService(SqlBatchCommit, batch))
|
|
59
|
+
: Option.isSome(parent)
|
|
60
|
+
? executor.run(work)
|
|
61
|
+
: executor
|
|
62
|
+
.operation(
|
|
63
|
+
work.pipe(
|
|
64
|
+
Effect.mapError((error) =>
|
|
65
|
+
error instanceof PasswordIdentifierTaken ? error : PasswordUnavailable.make({}),
|
|
66
|
+
),
|
|
65
67
|
),
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
68
|
+
)
|
|
69
|
+
.pipe(
|
|
70
|
+
Effect.catchCause((cause) =>
|
|
71
|
+
cause.reasons.length > 0 &&
|
|
72
|
+
cause.reasons.every(
|
|
73
|
+
(reason) =>
|
|
74
|
+
Cause.isFailReason(reason) && reason.error instanceof PasswordIdentifierTaken,
|
|
75
|
+
)
|
|
76
|
+
? executor.run(suppressed, "statement")
|
|
77
|
+
: Effect.failCause(cause),
|
|
78
|
+
),
|
|
79
|
+
Effect.mapError(() => PasswordUnavailable.make({})),
|
|
80
|
+
);
|
|
80
81
|
|
|
81
82
|
return {
|
|
82
83
|
register: (input, project) =>
|
|
@@ -86,26 +87,38 @@ export const makeRegistrationAuthority = Effect.fnUntraced(function* <R>(
|
|
|
86
87
|
? executor.run(store.prepare({ _tag: "Suppressed" }, project), "statement")
|
|
87
88
|
: run(
|
|
88
89
|
Effect.gen(function* () {
|
|
89
|
-
const
|
|
90
|
+
const provisioningInput = {
|
|
90
91
|
requestId: input.requestId,
|
|
91
92
|
identifier: input.identifier,
|
|
92
93
|
registration: input.registration,
|
|
93
|
-
}
|
|
94
|
+
};
|
|
94
95
|
|
|
95
|
-
const
|
|
96
|
+
const row = yield* Effect.gen(function* () {
|
|
97
|
+
if (typeof createSubject !== "function")
|
|
98
|
+
return yield* createSubject
|
|
99
|
+
.values(provisioningInput)
|
|
100
|
+
.pipe(Effect.flatMap(Schema.decodeEffect(SubjectValues)));
|
|
101
|
+
const subjectId = yield* createSubject(provisioningInput);
|
|
102
|
+
const nativeId = yield* mapping.subjectId.toNative(subjectId);
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
const rows =
|
|
105
|
+
yield* sql`select ${subject.fields("registration_subject_")} from ${subject.name} where ${subject.column(s.id)} = ${subject.value(s.id, nativeId)} limit 2 ${lock}`;
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
if (rows.length !== 1) return yield* PasswordUnavailable.make({});
|
|
108
|
+
|
|
109
|
+
return subject.decode(rows[0]!, "registration_subject_");
|
|
110
|
+
});
|
|
102
111
|
|
|
103
112
|
if (!s.isActiveStatus(row[s.status])) return yield* PasswordUnavailable.make({});
|
|
113
|
+
const subjectId = yield* mapping.subjectId.toSubject(row[s.id]);
|
|
104
114
|
|
|
105
115
|
const securityRevision = yield* Schema.decodeUnknownEffect(SecurityRevision)(
|
|
106
116
|
row[s.securityRevision],
|
|
107
117
|
);
|
|
108
118
|
|
|
119
|
+
if (typeof createSubject !== "function")
|
|
120
|
+
yield* store.stage(sql`${subject.insert(row)}`);
|
|
121
|
+
|
|
109
122
|
const identifierRevision = SecurityRevision.make(yield* randomId);
|
|
110
123
|
const credentialId = yield* randomId;
|
|
111
124
|
const credentialRevision = SecurityRevision.make(yield* randomId);
|
|
@@ -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 =
|
|
@@ -195,7 +210,9 @@ export const makeNativeAuthenticationAuthorityServices = Effect.fnUntraced(funct
|
|
|
195
210
|
: yield* consume(input.pending, input.evidence, current.native);
|
|
196
211
|
|
|
197
212
|
const deadline = Math.min(assessment.validUntil, expires, pendingExpiry ?? Infinity);
|
|
198
|
-
|
|
213
|
+
|
|
214
|
+
yield* state.guardPolicy(current);
|
|
215
|
+
const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${deadline}`;
|
|
199
216
|
|
|
200
217
|
if (batch !== undefined) {
|
|
201
218
|
yield* appendSqlBatchStatement(sqlBatchAssertion(sql, condition));
|
|
@@ -224,6 +241,7 @@ export const makeNativeAuthenticationAuthorityServices = Effect.fnUntraced(funct
|
|
|
224
241
|
if (rows.length !== 1) return yield* PendingAuthenticationInvalid.make({});
|
|
225
242
|
}
|
|
226
243
|
}).pipe(
|
|
244
|
+
Effect.provideService(AuthenticationClock, state.clockPolicy),
|
|
227
245
|
Effect.mapError((cause) =>
|
|
228
246
|
PersistenceMappingError.make({ operation: "decode", cause }),
|
|
229
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,
|
|
@@ -129,7 +132,9 @@ export const makeNativePendingAuthenticationServices = Effect.fnUntraced(functio
|
|
|
129
132
|
|
|
130
133
|
const record: PendingAuthenticationRecord<Claims> = { ...input, version };
|
|
131
134
|
const snapshot = yield* mapping.login.encode(record);
|
|
132
|
-
|
|
135
|
+
|
|
136
|
+
yield* state.guardPolicy(current);
|
|
137
|
+
const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${expires}`;
|
|
133
138
|
|
|
134
139
|
if (batch !== undefined)
|
|
135
140
|
yield* appendSqlBatchStatement(sqlBatchAssertion(sql, condition));
|
|
@@ -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 };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthenticationClock } from "@yielded/auth/Operations";
|
|
1
2
|
import { TokenDigest } from "@yielded/auth/Schema";
|
|
2
3
|
import {
|
|
3
4
|
SessionId,
|
|
@@ -50,14 +51,14 @@ export const makeConditionalSqlInsert = Effect.fnUntraced(function* () {
|
|
|
50
51
|
};
|
|
51
52
|
});
|
|
52
53
|
|
|
53
|
-
/** Security columns
|
|
54
|
-
*
|
|
55
|
-
* copy of prepared metadata. Claims/provenance decoding remains consumer-owned. */
|
|
54
|
+
/** Security columns own live time bounds and commit-time issuedAt adjustments,
|
|
55
|
+
* independently of prepared JSON metadata. Claims/provenance decoding remains consumer-owned. */
|
|
56
56
|
export const makeNativeSessionRecords = Effect.fnUntraced(function* <Claims>(
|
|
57
57
|
tables: NativeSqlTables,
|
|
58
58
|
mapping: NativeSessionRecordMapping<Claims>,
|
|
59
59
|
) {
|
|
60
60
|
const sql = (yield* SqlClient).withoutTransforms();
|
|
61
|
+
const { futureToleranceMillis } = yield* AuthenticationClock;
|
|
61
62
|
|
|
62
63
|
const s = mapping.session,
|
|
63
64
|
table = tables(s.table),
|
|
@@ -71,7 +72,8 @@ export const makeNativeSessionRecords = Effect.fnUntraced(function* <Claims>(
|
|
|
71
72
|
|
|
72
73
|
const live = (t: SqlTable) =>
|
|
73
74
|
sql.and([
|
|
74
|
-
sql`${millis(t, s.issuedAt)} <= ${now}`,
|
|
75
|
+
sql`${millis(t, s.issuedAt)} <= ${now} + ${futureToleranceMillis}`,
|
|
76
|
+
sql`${millis(t, s.issuedAt)} < ${millis(t, s.expiresAt)}`,
|
|
75
77
|
sql`${millis(t, s.expiresAt)} > ${now}`,
|
|
76
78
|
sql`${millis(t, s.absoluteExpiresAt)} >= ${millis(t, s.expiresAt)}`,
|
|
77
79
|
]);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthenticationClock } from "@yielded/auth/Operations";
|
|
1
2
|
import { reportAuthFailure } from "@yielded/auth/Persistence";
|
|
2
3
|
import type { SubjectId } from "@yielded/auth/Schema";
|
|
3
4
|
import {
|
|
@@ -14,12 +15,13 @@ import {
|
|
|
14
15
|
} from "@yielded/auth/Sessions";
|
|
15
16
|
import { Cause, Effect, Schema, DateTime } from "effect";
|
|
16
17
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
17
|
-
import type
|
|
18
|
+
import { fragment, parameter, type Fragment } from "effect/sql/Statement";
|
|
18
19
|
|
|
19
20
|
import { PersistenceMappingError } from "./mapping-error";
|
|
20
21
|
import type { SessionAuthorityTables, SessionExecution } from "./models/session-model";
|
|
21
|
-
import type
|
|
22
|
+
import { nativeSqlAlias, type NativeSqlTables, type SqlTable } from "./native-sql-table";
|
|
22
23
|
import { anySqlCondition, exactSqlText } from "./sql-change";
|
|
24
|
+
import { guardSqlitePolicy, type SqlitePolicyValue } from "./sqlite-policy-guard";
|
|
23
25
|
import type { AnyTableModel } from "./table-model";
|
|
24
26
|
|
|
25
27
|
export const sessionUnavailable = () => SessionUnavailable.make({});
|
|
@@ -72,6 +74,7 @@ export type NativeSessionAuthorityMapping = SessionAuthorityTables<
|
|
|
72
74
|
export interface NativeSessionAuthority {
|
|
73
75
|
readonly native: unknown;
|
|
74
76
|
readonly row: Record<string, unknown>;
|
|
77
|
+
readonly policyValues: ReadonlyArray<SqlitePolicyValue>;
|
|
75
78
|
readonly revision: AuthenticationRevision;
|
|
76
79
|
readonly requirement: AuthenticationRequirement;
|
|
77
80
|
readonly now: number;
|
|
@@ -96,6 +99,7 @@ export const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (
|
|
|
96
99
|
batch = false,
|
|
97
100
|
) {
|
|
98
101
|
const sql = (yield* SqlClient).withoutTransforms();
|
|
102
|
+
const clockPolicy = yield* AuthenticationClock;
|
|
99
103
|
|
|
100
104
|
const s = mapping.subject,
|
|
101
105
|
c = mapping.credential;
|
|
@@ -133,13 +137,15 @@ export const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (
|
|
|
133
137
|
const decode = Effect.fnUntraced(function* (
|
|
134
138
|
subjectId: SubjectId,
|
|
135
139
|
native: unknown,
|
|
136
|
-
|
|
140
|
+
projected: Record<string, unknown>,
|
|
137
141
|
factors: ReadonlyArray<Record<string, unknown>>,
|
|
138
142
|
instant: unknown,
|
|
139
143
|
): Effect.fn.Return<
|
|
140
144
|
NativeSessionAuthority | undefined,
|
|
141
145
|
PersistenceMappingError | Schema.SchemaError
|
|
142
146
|
> {
|
|
147
|
+
const row = subject.decode(projected, "s_");
|
|
148
|
+
|
|
143
149
|
sessionInvariant((yield* mapping.subjectId.toSubject(row[s.id])) === subjectId);
|
|
144
150
|
if (!s.isActiveStatus(row[s.status])) return undefined;
|
|
145
151
|
const credentials: { credentialId: string; revision: string }[] = [];
|
|
@@ -161,6 +167,16 @@ export const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (
|
|
|
161
167
|
return {
|
|
162
168
|
native,
|
|
163
169
|
row,
|
|
170
|
+
// Compare the selected SQL representation, without round-tripping through
|
|
171
|
+
// application codecs (for example, JSON whitespace or date precision).
|
|
172
|
+
policyValues: batch
|
|
173
|
+
? (s.requirementColumns ?? []).map((key) => ({
|
|
174
|
+
column: subject.selectedColumn(key),
|
|
175
|
+
value: fragment([
|
|
176
|
+
parameter(projected[nativeSqlAlias("s_", subject.keys.indexOf(key))]),
|
|
177
|
+
]),
|
|
178
|
+
}))
|
|
179
|
+
: [],
|
|
164
180
|
revision: yield* Schema.decodeUnknownEffect(AuthenticationRevision)({
|
|
165
181
|
subjectId,
|
|
166
182
|
securityRevision: row[s.securityRevision],
|
|
@@ -188,7 +204,7 @@ export const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (
|
|
|
188
204
|
return yield* decode(
|
|
189
205
|
subjectId,
|
|
190
206
|
native,
|
|
191
|
-
|
|
207
|
+
owners[0],
|
|
192
208
|
factors.map((row) => credential.decode(row, "c_")),
|
|
193
209
|
owners[0].engine_now,
|
|
194
210
|
);
|
|
@@ -202,18 +218,23 @@ export const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (
|
|
|
202
218
|
return yield* decode(
|
|
203
219
|
subjectId,
|
|
204
220
|
native,
|
|
205
|
-
|
|
221
|
+
rows[0],
|
|
206
222
|
rows.map((row) => joinedCredential.decode(row, "c_")),
|
|
207
223
|
rows[0].engine_now,
|
|
208
224
|
);
|
|
209
225
|
});
|
|
210
226
|
|
|
211
|
-
const
|
|
212
|
-
if (!batch) return
|
|
227
|
+
const guardPolicy = Effect.fnUntraced(function* (current: NativeSessionAuthority) {
|
|
228
|
+
if (!batch) return;
|
|
213
229
|
sessionInvariant(s.requirementColumns !== undefined);
|
|
214
230
|
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
yield* guardSqlitePolicy({
|
|
232
|
+
name: "session-subject-policy",
|
|
233
|
+
table: subject.name,
|
|
234
|
+
owner: id(subject, s.id, current.native),
|
|
235
|
+
values: current.policyValues,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
217
238
|
|
|
218
239
|
const authorityCondition = (revision: AuthenticationRevision, native: unknown): Fragment =>
|
|
219
240
|
sql.and([
|
|
@@ -227,6 +248,7 @@ export const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (
|
|
|
227
248
|
|
|
228
249
|
return {
|
|
229
250
|
sql,
|
|
251
|
+
clockPolicy,
|
|
230
252
|
subject,
|
|
231
253
|
credential,
|
|
232
254
|
joinedSubject,
|
|
@@ -242,7 +264,7 @@ export const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (
|
|
|
242
264
|
decode,
|
|
243
265
|
read,
|
|
244
266
|
authorityCondition,
|
|
245
|
-
|
|
267
|
+
guardPolicy,
|
|
246
268
|
};
|
|
247
269
|
});
|
|
248
270
|
|