@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,36 +1,42 @@
|
|
|
1
1
|
import { exactSqlText } from "./sql-change.mjs";
|
|
2
|
-
import { makeNativeSessionAuthorityState } from "./session-native-state.mjs";
|
|
2
|
+
import { makeNativeSessionAuthorityState, sessionInvariant, sessionUnavailable } from "./session-native-state.mjs";
|
|
3
|
+
import { canJoinTextColumns } from "./storage-validation.mjs";
|
|
3
4
|
import { makeNativeSessionPending } from "./session-native-pending.mjs";
|
|
4
5
|
import { Effect } from "effect";
|
|
5
6
|
//#region src/internal/session-native-pending-read.ts
|
|
6
|
-
/** Both
|
|
7
|
-
*
|
|
8
|
-
* validate each decoded owner before returning current policy. */
|
|
7
|
+
/** Both ports re-read the pending row with current authority in one snapshot.
|
|
8
|
+
* Discovery only selects the independently bound owner for incompatible IDs. */
|
|
9
9
|
const makeNativeSessionPendingReader = Effect.fnUntraced(function* (tables, mapping, batch = false) {
|
|
10
10
|
const state = yield* makeNativeSessionAuthorityState(tables, mapping, batch);
|
|
11
11
|
const pending = yield* makeNativeSessionPending(tables, mapping, batch);
|
|
12
|
+
const joinedIds = yield* canJoinTextColumns([
|
|
13
|
+
pending.table.unencodedTextColumn?.(pending.p.subjectId),
|
|
14
|
+
state.subject.unencodedTextColumn?.(mapping.subject.id),
|
|
15
|
+
state.credential.unencodedTextColumn?.(mapping.credential.subjectId)
|
|
16
|
+
]).pipe(Effect.mapError(sessionUnavailable));
|
|
12
17
|
return {
|
|
13
18
|
state,
|
|
14
19
|
pending,
|
|
15
20
|
read: Effect.fnUntraced(function* (kind, digest) {
|
|
16
21
|
const { sql, now } = state, { p, table } = pending;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return authority === void 0 ? void 0 : {
|
|
25
|
-
record,
|
|
26
|
-
authority
|
|
27
|
-
};
|
|
22
|
+
let discoveredSubjectId;
|
|
23
|
+
let nativeId;
|
|
24
|
+
if (!joinedIds) {
|
|
25
|
+
const selected = yield* pending.read(kind, digest);
|
|
26
|
+
if (selected === void 0) return void 0;
|
|
27
|
+
discoveredSubjectId = selected.record.subjectId;
|
|
28
|
+
nativeId = yield* mapping.subjectId.toNative(discoveredSubjectId);
|
|
28
29
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const pt = table.as("authentication_pending"), st = state.joinedSubject, ct = state.joinedCredential, s = mapping.subject, c = mapping.credential;
|
|
31
|
+
const bindOwner = (table, key) => joinedIds ? exactSqlText(sql, table.column(key), pt.column(p.subjectId)) : state.id(table, key, nativeId);
|
|
32
|
+
const rows = 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`;
|
|
33
|
+
if (rows[0] === void 0) return void 0;
|
|
34
|
+
const record = yield* pending.decode(kind, pt.decode(rows[0], "p_"));
|
|
35
|
+
sessionInvariant(joinedIds || record.subjectId === discoveredSubjectId);
|
|
36
|
+
const owner = yield* mapping.subjectId.toNative(record.subjectId);
|
|
37
|
+
const authority = yield* state.decode(record.subjectId, owner, rows[0], rows.map((row) => ct.decode(row, "c_")), rows[0].engine_now);
|
|
38
|
+
return authority === void 0 || authority.now >= record.expiresAtMillis ? void 0 : {
|
|
39
|
+
record,
|
|
34
40
|
authority
|
|
35
41
|
};
|
|
36
42
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-native-pending-read.mjs","names":[],"sources":["../../src/internal/session-native-pending-read.ts"],"sourcesContent":["import type { TokenDigest } from \"@yielded/auth/Schema\";\nimport type { PendingAuthenticationKind } from \"@yielded/auth/Sessions\";\nimport { Effect } from \"effect\";\n\nimport type { NativeSqlTables } from \"./native-sql-table\";\nimport { makeNativeSessionPending } from \"./session-native-pending\";\nimport {\n makeNativeSessionAuthorityState,\n type NativeSessionAuthorityMapping,\n} from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\n\n/** Both
|
|
1
|
+
{"version":3,"file":"session-native-pending-read.mjs","names":[],"sources":["../../src/internal/session-native-pending-read.ts"],"sourcesContent":["import type { SubjectId, TokenDigest } from \"@yielded/auth/Schema\";\nimport type { PendingAuthenticationKind } from \"@yielded/auth/Sessions\";\nimport { Effect } from \"effect\";\n\nimport type { NativeSqlTables } from \"./native-sql-table\";\nimport { makeNativeSessionPending } from \"./session-native-pending\";\nimport {\n makeNativeSessionAuthorityState,\n sessionInvariant,\n sessionUnavailable,\n type NativeSessionAuthorityMapping,\n} from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\nimport { canJoinTextColumns } from \"./storage-validation\";\n\n/** Both ports re-read the pending row with current authority in one snapshot.\n * Discovery only selects the independently bound owner for incompatible IDs. */\nexport const makeNativeSessionPendingReader = Effect.fnUntraced(function* (\n tables: NativeSqlTables,\n mapping: NativeSessionAuthorityMapping & Parameters<typeof makeNativeSessionPending>[1],\n batch = false,\n) {\n const state = yield* makeNativeSessionAuthorityState(tables, mapping, batch);\n const pending = yield* makeNativeSessionPending(tables, mapping, batch);\n\n const joinedIds = yield* canJoinTextColumns([\n pending.table.unencodedTextColumn?.(pending.p.subjectId),\n state.subject.unencodedTextColumn?.(mapping.subject.id),\n state.credential.unencodedTextColumn?.(mapping.credential.subjectId),\n ]).pipe(Effect.mapError(sessionUnavailable));\n\n const read = Effect.fnUntraced(function* (kind: PendingAuthenticationKind, digest: TokenDigest) {\n const { sql, now } = state,\n { p, table } = pending;\n\n let discoveredSubjectId: SubjectId | undefined;\n let nativeId: unknown;\n\n if (!joinedIds) {\n const selected = yield* pending.read(kind, digest);\n\n if (selected === undefined) return undefined;\n discoveredSubjectId = selected.record.subjectId;\n nativeId = yield* mapping.subjectId.toNative(discoveredSubjectId);\n }\n\n const pt = table.as(\"authentication_pending\"),\n st = state.joinedSubject,\n ct = state.joinedCredential,\n s = mapping.subject,\n c = mapping.credential;\n\n const bindOwner = (table: typeof pt, key: string) =>\n joinedIds\n ? exactSqlText(sql, table.column(key), pt.column(p.subjectId))\n : state.id(table, key, nativeId);\n\n const rows =\n 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`;\n\n if (rows[0] === undefined) return undefined;\n const record = yield* pending.decode(kind, pt.decode(rows[0], \"p_\"));\n\n sessionInvariant(joinedIds || record.subjectId === discoveredSubjectId);\n const owner = yield* mapping.subjectId.toNative(record.subjectId);\n\n const authority = yield* state.decode(\n record.subjectId,\n owner,\n rows[0],\n rows.map((row) => ct.decode(row, \"c_\")),\n rows[0].engine_now,\n );\n\n return authority === undefined || authority.now >= record.expiresAtMillis\n ? undefined\n : { record, authority };\n });\n\n return { state, pending, read };\n});\n"],"mappings":";;;;;;;;AAiBA,MAAa,iCAAiC,OAAO,WAAW,WAC9D,QACA,SACA,QAAQ,OACR;CACA,MAAM,QAAQ,OAAO,gCAAgC,QAAQ,SAAS,KAAK;CAC3E,MAAM,UAAU,OAAO,yBAAyB,QAAQ,SAAS,KAAK;CAEtE,MAAM,YAAY,OAAO,mBAAmB;EAC1C,QAAQ,MAAM,sBAAsB,QAAQ,EAAE,SAAS;EACvD,MAAM,QAAQ,sBAAsB,QAAQ,QAAQ,EAAE;EACtD,MAAM,WAAW,sBAAsB,QAAQ,WAAW,SAAS;CACrE,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,kBAAkB,CAAC;CAkD3C,OAAO;EAAE;EAAO;EAAS,MAhDZ,OAAO,WAAW,WAAW,MAAiC,QAAqB;GAC9F,MAAM,EAAE,KAAK,QAAQ,OACnB,EAAE,GAAG,UAAU;GAEjB,IAAI;GACJ,IAAI;GAEJ,IAAI,CAAC,WAAW;IACd,MAAM,WAAW,OAAO,QAAQ,KAAK,MAAM,MAAM;IAEjD,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;IACnC,sBAAsB,SAAS,OAAO;IACtC,WAAW,OAAO,QAAQ,UAAU,SAAS,mBAAmB;GAClE;GAEA,MAAM,KAAK,MAAM,GAAG,wBAAwB,GAC1C,KAAK,MAAM,eACX,KAAK,MAAM,kBACX,IAAI,QAAQ,SACZ,IAAI,QAAQ;GAEd,MAAM,aAAa,OAAkB,QACnC,YACI,aAAa,KAAK,MAAM,OAAO,GAAG,GAAG,GAAG,OAAO,EAAE,SAAS,CAAC,IAC3D,MAAM,GAAG,OAAO,KAAK,QAAQ;GAEnC,MAAM,OACJ,OAAO,GAAG,UAAU,GAAG,OAAO,IAAI,EAAE,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,IAAI,sBAAsB,GAAG,KAAK,QAAQ,GAAG,KAAK,MAAM,UAAU,IAAI,EAAE,EAAE,EAAE,OAAO,MAAM,cAAc,EAAE,EAAE,aAAa,GAAG,KAAK,MAAM,UAAU,IAAI,EAAE,SAAS,EAAE,OAAO,MAAM,iBAAiB,EAAE,EAAE,SAAS,QAAQ,UAAU,IAAI,MAAM,MAAM,IAAI,YAAY,GAAG,KAAK,GAAG,QAAQ,UAAU,IAAI,EAAE,SAAS,IAAI;GAEhY,IAAI,KAAK,OAAO,KAAA,GAAW,OAAO,KAAA;GAClC,MAAM,SAAS,OAAO,QAAQ,OAAO,MAAM,GAAG,OAAO,KAAK,IAAI,IAAI,CAAC;GAEnE,iBAAiB,aAAa,OAAO,cAAc,mBAAmB;GACtE,MAAM,QAAQ,OAAO,QAAQ,UAAU,SAAS,OAAO,SAAS;GAEhE,MAAM,YAAY,OAAO,MAAM,OAC7B,OAAO,WACP,OACA,KAAK,IACL,KAAK,KAAK,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC,GACtC,KAAK,EAAE,CAAC,UACV;GAEA,OAAO,cAAc,KAAA,KAAa,UAAU,OAAO,OAAO,kBACtD,KAAA,IACA;IAAE;IAAQ;GAAU;EAC1B,CAE4B;CAAE;AAChC,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { sessionInvariant } from "./session-native-state.mjs";
|
|
|
3
3
|
import { DateTime, Effect, Schema } from "effect";
|
|
4
4
|
import { SecurityRevision, SessionAuthenticationProvenance, SessionCredentialVersion, SessionId, SessionMetadata } from "@yielded/auth/Sessions";
|
|
5
5
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
6
|
+
import { AuthenticationClock } from "@yielded/auth/Operations";
|
|
6
7
|
import { TokenDigest } from "@yielded/auth/Schema";
|
|
7
8
|
//#region src/internal/session-native-record.ts
|
|
8
9
|
const metadataJson = Schema.encodeSync(Schema.fromJsonString(SessionMetadata));
|
|
@@ -16,16 +17,17 @@ const makeConditionalSqlInsert = Effect.fnUntraced(function* () {
|
|
|
16
17
|
return sql`insert into ${table.name} (${sql.join(", ", false)(fields.map(([key]) => table.columnName(key)))}) select ${sql.join(", ", false)(fields.map(([key, value]) => table.value(key, value)))} where ${condition}`;
|
|
17
18
|
};
|
|
18
19
|
});
|
|
19
|
-
/** Security columns
|
|
20
|
-
*
|
|
21
|
-
* copy of prepared metadata. Claims/provenance decoding remains consumer-owned. */
|
|
20
|
+
/** Security columns own live time bounds and commit-time issuedAt adjustments,
|
|
21
|
+
* independently of prepared JSON metadata. Claims/provenance decoding remains consumer-owned. */
|
|
22
22
|
const makeNativeSessionRecords = Effect.fnUntraced(function* (tables, mapping) {
|
|
23
23
|
const sql = (yield* SqlClient).withoutTransforms();
|
|
24
|
+
const { futureToleranceMillis } = yield* AuthenticationClock;
|
|
24
25
|
const s = mapping.session, table = tables(s.table), now = tables.expression(mapping.clock.engineNowMillis);
|
|
25
26
|
const exact = (t, key, value) => exactSqlText(sql, t.column(key), t.value(key, value));
|
|
26
27
|
const millis = (t, key) => tables.expression(mapping.clock.toMillis(t.column(key)));
|
|
27
28
|
const live = (t) => sql.and([
|
|
28
|
-
sql`${millis(t, s.issuedAt)} <= ${now}`,
|
|
29
|
+
sql`${millis(t, s.issuedAt)} <= ${now} + ${futureToleranceMillis}`,
|
|
30
|
+
sql`${millis(t, s.issuedAt)} < ${millis(t, s.expiresAt)}`,
|
|
29
31
|
sql`${millis(t, s.expiresAt)} > ${now}`,
|
|
30
32
|
sql`${millis(t, s.absoluteExpiresAt)} >= ${millis(t, s.expiresAt)}`
|
|
31
33
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-native-record.mjs","names":[],"sources":["../../src/internal/session-native-record.ts"],"sourcesContent":["import { TokenDigest } from \"@yielded/auth/Schema\";\nimport {\n SessionId,\n SessionMetadata,\n SessionAuthenticationProvenance,\n SessionCredentialVersion,\n SecurityRevision,\n type StatefulSessionRecord,\n} from \"@yielded/auth/Sessions\";\nimport { DateTime, Effect, Schema } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport type { Fragment } from \"effect/sql/Statement\";\n\nimport type { SubjectIdCodec } from \"./models/common\";\nimport type { StatefulSessionTables, SessionExecution } from \"./models/session-model\";\nimport type { NativeSqlTables, SqlTable } from \"./native-sql-table\";\nimport { sessionInvariant } from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport type NativeSessionRecordMapping<Claims> = StatefulSessionTables<\n Claims,\n AnyTableModel,\n unknown,\n unknown\n> &\n Pick<SessionExecution, \"clock\"> & { readonly subjectId: SubjectIdCodec<unknown> };\n\nconst metadataJson = Schema.encodeSync(Schema.fromJsonString(SessionMetadata));\nconst provenanceJson = Schema.encodeSync(Schema.fromJsonString(SessionAuthenticationProvenance));\n\nexport const sameSessionRecord = <Claims>(\n left: StatefulSessionRecord<Claims>,\n right: StatefulSessionRecord<Claims>,\n) =>\n left.digest === right.digest &&\n left.credentialVersion === right.credentialVersion &&\n metadataJson(left) === metadataJson(right) &&\n provenanceJson(left.provenance) === provenanceJson(right.provenance);\n\nexport const makeConditionalSqlInsert = Effect.fnUntraced(function* () {\n const sql = (yield* SqlClient).withoutTransforms();\n\n return (table: SqlTable, values: Readonly<Record<string, unknown>>, condition: Fragment) => {\n const fields = Object.entries(values).filter(([, value]) => value !== undefined);\n\n sessionInvariant(fields.length > 0);\n\n return sql`insert into ${table.name} (${sql.join(\", \", false)(fields.map(([key]) => table.columnName(key)))}) select ${sql.join(\", \", false)(fields.map(([key, value]) => table.value(key, value)))} where ${condition}`;\n };\n});\n\n/** Security columns
|
|
1
|
+
{"version":3,"file":"session-native-record.mjs","names":[],"sources":["../../src/internal/session-native-record.ts"],"sourcesContent":["import { AuthenticationClock } from \"@yielded/auth/Operations\";\nimport { TokenDigest } from \"@yielded/auth/Schema\";\nimport {\n SessionId,\n SessionMetadata,\n SessionAuthenticationProvenance,\n SessionCredentialVersion,\n SecurityRevision,\n type StatefulSessionRecord,\n} from \"@yielded/auth/Sessions\";\nimport { DateTime, Effect, Schema } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport type { Fragment } from \"effect/sql/Statement\";\n\nimport type { SubjectIdCodec } from \"./models/common\";\nimport type { StatefulSessionTables, SessionExecution } from \"./models/session-model\";\nimport type { NativeSqlTables, SqlTable } from \"./native-sql-table\";\nimport { sessionInvariant } from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport type NativeSessionRecordMapping<Claims> = StatefulSessionTables<\n Claims,\n AnyTableModel,\n unknown,\n unknown\n> &\n Pick<SessionExecution, \"clock\"> & { readonly subjectId: SubjectIdCodec<unknown> };\n\nconst metadataJson = Schema.encodeSync(Schema.fromJsonString(SessionMetadata));\nconst provenanceJson = Schema.encodeSync(Schema.fromJsonString(SessionAuthenticationProvenance));\n\nexport const sameSessionRecord = <Claims>(\n left: StatefulSessionRecord<Claims>,\n right: StatefulSessionRecord<Claims>,\n) =>\n left.digest === right.digest &&\n left.credentialVersion === right.credentialVersion &&\n metadataJson(left) === metadataJson(right) &&\n provenanceJson(left.provenance) === provenanceJson(right.provenance);\n\nexport const makeConditionalSqlInsert = Effect.fnUntraced(function* () {\n const sql = (yield* SqlClient).withoutTransforms();\n\n return (table: SqlTable, values: Readonly<Record<string, unknown>>, condition: Fragment) => {\n const fields = Object.entries(values).filter(([, value]) => value !== undefined);\n\n sessionInvariant(fields.length > 0);\n\n return sql`insert into ${table.name} (${sql.join(\", \", false)(fields.map(([key]) => table.columnName(key)))}) select ${sql.join(\", \", false)(fields.map(([key, value]) => table.value(key, value)))} where ${condition}`;\n };\n});\n\n/** Security columns own live time bounds and commit-time issuedAt adjustments,\n * independently of prepared JSON metadata. Claims/provenance decoding remains consumer-owned. */\nexport const makeNativeSessionRecords = Effect.fnUntraced(function* <Claims>(\n tables: NativeSqlTables,\n mapping: NativeSessionRecordMapping<Claims>,\n) {\n const sql = (yield* SqlClient).withoutTransforms();\n const { futureToleranceMillis } = yield* AuthenticationClock;\n\n const s = mapping.session,\n table = tables(s.table),\n now = tables.expression(mapping.clock.engineNowMillis);\n\n const exact = (t: SqlTable, key: string, value: unknown) =>\n exactSqlText(sql, t.column(key), t.value(key, value));\n\n const millis = (t: SqlTable, key: string) =>\n tables.expression(mapping.clock.toMillis(t.column(key)));\n\n const live = (t: SqlTable) =>\n sql.and([\n sql`${millis(t, s.issuedAt)} <= ${now} + ${futureToleranceMillis}`,\n sql`${millis(t, s.issuedAt)} < ${millis(t, s.expiresAt)}`,\n sql`${millis(t, s.expiresAt)} > ${now}`,\n sql`${millis(t, s.absoluteExpiresAt)} >= ${millis(t, s.expiresAt)}`,\n ]);\n\n const decode = Effect.fnUntraced(function* (row: Record<string, unknown>) {\n const decoded = yield* s.decode(row);\n const subjectId = yield* mapping.subjectId.toSubject(row[s.subjectId]);\n const sessionId = yield* mapping.sessionId.toSession(row[s.sessionId]);\n const digest = yield* Schema.decodeUnknownEffect(TokenDigest)(row[s.digest]);\n const revision = yield* Schema.decodeUnknownEffect(SecurityRevision)(row[s.securityRevision]);\n const issuedAt = DateTime.makeUnsafe(mapping.clock.decodeInstant(row[s.issuedAt]));\n const expiresAt = mapping.clock.decodeInstant(row[s.expiresAt]);\n const absolute = mapping.clock.decodeInstant(row[s.absoluteExpiresAt]);\n\n sessionInvariant(\n decoded.subjectId === subjectId &&\n decoded.sessionId === sessionId &&\n decoded.digest === digest &&\n decoded.securityRevision === revision &&\n DateTime.toEpochMillis(decoded.expiresAt) === expiresAt &&\n DateTime.toEpochMillis(decoded.absoluteExpiresAt) === absolute,\n );\n yield* Schema.decodeEffect(SessionId)(sessionId);\n yield* Schema.decodeEffect(SessionCredentialVersion)(decoded.credentialVersion);\n\n return { ...decoded, issuedAt };\n });\n\n const owner = Effect.fnUntraced(function* (\n record: Pick<StatefulSessionRecord<Claims>, \"subjectId\" | \"sessionId\" | \"digest\">,\n ) {\n const nativeSubject = yield* mapping.subjectId.toNative(record.subjectId),\n nativeSession = yield* mapping.sessionId.toNative(record.sessionId);\n\n return sql.and([\n sql`${table.column(s.subjectId)} = ${table.value(s.subjectId, nativeSubject)}`,\n sql`${table.column(s.sessionId)} = ${table.value(s.sessionId, nativeSession)}`,\n exact(table, s.digest, record.digest),\n ]);\n });\n\n // These are the semantic values supplied by the session encoder for this\n // write, not an observed whole-row snapshot or unrelated application columns.\n const encodedCondition = (values: Readonly<Record<string, unknown>>) =>\n sql.and(\n Object.entries(values)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) =>\n value === null\n ? sql`${table.column(key)} is null`\n : typeof value === \"string\"\n ? exact(table, key, value)\n : sql`${table.column(key)} = ${table.value(key, value)}`,\n ),\n );\n\n const read = Effect.fnUntraced(function* (digest: TokenDigest) {\n const rows =\n yield* sql`select ${table.fields(\"session_\")} from ${table.name} where ${exact(table, s.digest, digest)} and ${live(table)} limit 2`;\n\n sessionInvariant(rows.length <= 1);\n\n return rows[0] === undefined ? undefined : yield* decode(table.decode(rows[0], \"session_\"));\n });\n\n return { sql, s, table, now, exact, millis, live, decode, owner, read, encodedCondition };\n});\n"],"mappings":"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"}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { PersistenceMappingError } from "./mapping-error.mjs";
|
|
2
|
+
import { nativeSqlAlias } from "./native-sql-table.mjs";
|
|
2
3
|
import { anySqlCondition, exactSqlText } from "./sql-change.mjs";
|
|
4
|
+
import { guardSqlitePolicy } from "./sqlite-policy-guard.mjs";
|
|
3
5
|
import { Cause, DateTime, Effect, Schema } from "effect";
|
|
6
|
+
import { fragment, parameter } from "effect/sql/Statement";
|
|
4
7
|
import { reportAuthFailure } from "@yielded/auth/Persistence";
|
|
5
8
|
import { AuthenticationRevision, PendingAuthenticationInvalid, SessionConflict, SessionInvalid, SessionStepUpInvalid, SessionUnavailable, StaleAuthentication, assessAuthentication } from "@yielded/auth/Sessions";
|
|
6
9
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
10
|
+
import { AuthenticationClock } from "@yielded/auth/Operations";
|
|
7
11
|
//#region src/internal/session-native-state.ts
|
|
8
12
|
const sessionUnavailable = () => SessionUnavailable.make({});
|
|
9
13
|
const sessionInvariant = (condition) => {
|
|
@@ -22,6 +26,7 @@ const sameSessionRevision = (left, right) => left.subjectId === right.subjectId
|
|
|
22
26
|
* precede every credential lock, including inside an application coordinator. */
|
|
23
27
|
const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, mapping, batch = false) {
|
|
24
28
|
const sql = (yield* SqlClient).withoutTransforms();
|
|
29
|
+
const clockPolicy = yield* AuthenticationClock;
|
|
25
30
|
const s = mapping.subject, c = mapping.credential;
|
|
26
31
|
if (batch) sessionInvariant(s.requirementColumns !== void 0);
|
|
27
32
|
const subject = tables(s.table), credential = tables(c.table);
|
|
@@ -44,7 +49,8 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
|
|
|
44
49
|
return id(table, c.status, c.activeStatusValue);
|
|
45
50
|
};
|
|
46
51
|
const requested = (table, ids) => anySqlCondition(ids.map((value) => exact(table, c.credentialId, value)));
|
|
47
|
-
const decode = Effect.fnUntraced(function* (subjectId, native,
|
|
52
|
+
const decode = Effect.fnUntraced(function* (subjectId, native, projected, factors, instant) {
|
|
53
|
+
const row = subject.decode(projected, "s_");
|
|
48
54
|
sessionInvariant((yield* mapping.subjectId.toSubject(row[s.id])) === subjectId);
|
|
49
55
|
if (!s.isActiveStatus(row[s.status])) return void 0;
|
|
50
56
|
const credentials = [];
|
|
@@ -66,6 +72,10 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
|
|
|
66
72
|
return {
|
|
67
73
|
native,
|
|
68
74
|
row,
|
|
75
|
+
policyValues: batch ? (s.requirementColumns ?? []).map((key) => ({
|
|
76
|
+
column: subject.selectedColumn(key),
|
|
77
|
+
value: fragment([parameter(projected[nativeSqlAlias("s_", subject.keys.indexOf(key))])])
|
|
78
|
+
})) : [],
|
|
69
79
|
revision: yield* Schema.decodeUnknownEffect(AuthenticationRevision)({
|
|
70
80
|
subjectId,
|
|
71
81
|
securityRevision: row[s.securityRevision],
|
|
@@ -83,17 +93,22 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
|
|
|
83
93
|
sessionInvariant(owners.length <= 1);
|
|
84
94
|
if (owners[0] === void 0) return void 0;
|
|
85
95
|
const factors = yield* sql`select ${credential.fields("c_")} from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)} order by ${credential.column(c.credentialId)} limit 65 ${lock}`;
|
|
86
|
-
return yield* decode(subjectId, native,
|
|
96
|
+
return yield* decode(subjectId, native, owners[0], factors.map((row) => credential.decode(row, "c_")), owners[0].engine_now);
|
|
87
97
|
}
|
|
88
98
|
const rows = yield* sql`select ${joinedSubject.fields("s_")}, ${joinedCredential.fields("c_")}, ${now} as engine_now from ${joinedSubject.name} left join ${joinedCredential.name} on ${id(joinedCredential, c.subjectId, native)} and ${activeCredential(joinedCredential)} where ${id(joinedSubject, s.id, native)} and ${activeSubject(joinedSubject)} limit 65`;
|
|
89
99
|
if (rows[0] === void 0) return void 0;
|
|
90
|
-
return yield* decode(subjectId, native,
|
|
100
|
+
return yield* decode(subjectId, native, rows[0], rows.map((row) => joinedCredential.decode(row, "c_")), rows[0].engine_now);
|
|
91
101
|
});
|
|
92
|
-
const
|
|
93
|
-
if (!batch) return
|
|
102
|
+
const guardPolicy = Effect.fnUntraced(function* (current) {
|
|
103
|
+
if (!batch) return;
|
|
94
104
|
sessionInvariant(s.requirementColumns !== void 0);
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
yield* guardSqlitePolicy({
|
|
106
|
+
name: "session-subject-policy",
|
|
107
|
+
table: subject.name,
|
|
108
|
+
owner: id(subject, s.id, current.native),
|
|
109
|
+
values: current.policyValues
|
|
110
|
+
});
|
|
111
|
+
});
|
|
97
112
|
const authorityCondition = (revision, native) => sql.and([
|
|
98
113
|
sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${exact(subject, s.securityRevision, revision.securityRevision)} and ${activeSubject(subject)})`,
|
|
99
114
|
sql`(select count(*) from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)}) = ${revision.credentials.length}`,
|
|
@@ -101,6 +116,7 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
|
|
|
101
116
|
]);
|
|
102
117
|
return {
|
|
103
118
|
sql,
|
|
119
|
+
clockPolicy,
|
|
104
120
|
subject,
|
|
105
121
|
credential,
|
|
106
122
|
joinedSubject,
|
|
@@ -116,7 +132,7 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
|
|
|
116
132
|
decode,
|
|
117
133
|
read,
|
|
118
134
|
authorityCondition,
|
|
119
|
-
|
|
135
|
+
guardPolicy
|
|
120
136
|
};
|
|
121
137
|
});
|
|
122
138
|
/** Evaluate freshness at the database sample. The final write also enforces the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-native-state.mjs","names":[],"sources":["../../src/internal/session-native-state.ts"],"sourcesContent":["import { reportAuthFailure } from \"@yielded/auth/Persistence\";\nimport type { SubjectId } from \"@yielded/auth/Schema\";\nimport {\n type AuthenticationRequirement,\n AuthenticationRevision,\n SessionConflict,\n SessionInvalid,\n SessionUnavailable,\n SessionStepUpInvalid,\n PendingAuthenticationInvalid,\n StaleAuthentication,\n assessAuthentication,\n type AuthenticationEvidence,\n} from \"@yielded/auth/Sessions\";\nimport { Cause, Effect, Schema, DateTime } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport type { Fragment } from \"effect/sql/Statement\";\n\nimport { PersistenceMappingError } from \"./mapping-error\";\nimport type { SessionAuthorityTables, SessionExecution } from \"./models/session-model\";\nimport type { NativeSqlTables, SqlTable } from \"./native-sql-table\";\nimport { anySqlCondition, exactSqlText } from \"./sql-change\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport const sessionUnavailable = () => SessionUnavailable.make({});\n\nexport const sessionInvariant: (condition: unknown) => asserts condition = (condition) => {\n if (!condition)\n throw PersistenceMappingError.make({ operation: \"decode\", cause: \"Invalid session authority\" });\n};\n\nexport type SessionDomainFailure =\n | SessionConflict\n | SessionInvalid\n | SessionUnavailable\n | SessionStepUpInvalid\n | PendingAuthenticationInvalid\n | StaleAuthentication;\n\nconst isDomainFailure = (error: unknown): error is SessionDomainFailure =>\n Schema.is(SessionConflict)(error) ||\n Schema.is(SessionInvalid)(error) ||\n Schema.is(SessionUnavailable)(error) ||\n Schema.is(SessionStepUpInvalid)(error) ||\n Schema.is(PendingAuthenticationInvalid)(error) ||\n Schema.is(StaleAuthentication)(error);\n\n/** Only schema-checked domain failures cross the owner boundary; SQL/mapping\n * failures remain Unavailable. The executor preserves every mixed cause reason. */\nexport const sessionFailure = <E>(\n error: E,\n): Extract<E, SessionDomainFailure> | SessionUnavailable =>\n isDomainFailure(error) ? (error as Extract<E, SessionDomainFailure>) : sessionUnavailable();\n\nexport const normalizeSessionOperation = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n effect.pipe(\n Effect.tapError((error) =>\n isDomainFailure(error)\n ? Effect.void\n : reportAuthFailure(\"auth-persistence\", Cause.fail(error)),\n ),\n Effect.mapError(sessionFailure),\n );\n\nexport type NativeSessionAuthorityMapping = SessionAuthorityTables<\n AnyTableModel,\n AnyTableModel,\n unknown\n> &\n SessionExecution;\n\nexport interface NativeSessionAuthority {\n readonly native: unknown;\n readonly row: Record<string, unknown>;\n readonly revision: AuthenticationRevision;\n readonly requirement: AuthenticationRequirement;\n readonly now: number;\n}\n\nexport const sameSessionRevision = (left: AuthenticationRevision, right: AuthenticationRevision) =>\n left.subjectId === right.subjectId &&\n left.securityRevision === right.securityRevision &&\n left.credentials.length === right.credentials.length &&\n new Set(left.credentials.map((f) => f.credentialId)).size === left.credentials.length &&\n left.credentials.every((f) =>\n right.credentials.some(\n (other) => other.credentialId === f.credentialId && other.revision === f.revision,\n ),\n );\n\n/** Known-subject joins bind each physical ID column independently. Subject locks\n * precede every credential lock, including inside an application coordinator. */\nexport const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (\n tables: NativeSqlTables,\n mapping: NativeSessionAuthorityMapping,\n batch = false,\n) {\n const sql = (yield* SqlClient).withoutTransforms();\n\n const s = mapping.subject,\n c = mapping.credential;\n\n if (batch) sessionInvariant(s.requirementColumns !== undefined);\n\n const subject = tables(s.table),\n credential = tables(c.table);\n\n const joinedSubject = subject.as(\"session_subject\"),\n joinedCredential = credential.as(\"session_factor\");\n\n const now = tables.expression(mapping.clock.engineNowMillis);\n const lock = sql.onDialectOrElse({ sqlite: () => sql``, orElse: () => sql`for update` });\n const nativeLocks = !batch && sql.onDialectOrElse({ sqlite: () => false, orElse: () => true });\n\n const exact = (table: SqlTable, key: string, value: unknown) =>\n exactSqlText(sql, table.column(key), table.value(key, value));\n\n const id = (table: SqlTable, key: string, value: unknown) =>\n sql`${table.column(key)} = ${table.value(key, value)}`;\n\n const activeSubject = (table: SqlTable) => id(table, s.status, s.activeStatusValue);\n\n const activeCredential = (table: SqlTable) => {\n if (c.status === undefined) return sql`1 = 1`;\n sessionInvariant(c.activeStatusValue !== undefined);\n\n return id(table, c.status, c.activeStatusValue);\n };\n\n const requested = (table: SqlTable, ids: ReadonlyArray<string>) =>\n anySqlCondition(ids.map((value) => exact(table, c.credentialId, value)));\n\n const decode = Effect.fnUntraced(function* (\n subjectId: SubjectId,\n native: unknown,\n row: Record<string, unknown>,\n factors: ReadonlyArray<Record<string, unknown>>,\n instant: unknown,\n ): Effect.fn.Return<\n NativeSessionAuthority | undefined,\n PersistenceMappingError | Schema.SchemaError\n > {\n sessionInvariant((yield* mapping.subjectId.toSubject(row[s.id])) === subjectId);\n if (!s.isActiveStatus(row[s.status])) return undefined;\n const credentials: { credentialId: string; revision: string }[] = [];\n\n for (const factor of factors) {\n if (factor[c.credentialId] === null || factor[c.credentialId] === undefined) continue;\n sessionInvariant((yield* mapping.subjectId.toSubject(factor[c.subjectId])) === subjectId);\n if (c.status !== undefined) sessionInvariant(c.isActiveStatus?.(factor[c.status]) === true);\n\n const value = yield* Schema.decodeUnknownEffect(\n Schema.Struct({ credentialId: Schema.String, revision: Schema.String }),\n )({ credentialId: factor[c.credentialId], revision: factor[c.revision] });\n\n sessionInvariant(!credentials.some((other) => other.credentialId === value.credentialId));\n credentials.push(value);\n }\n sessionInvariant(credentials.length <= 64);\n\n return {\n native,\n row,\n revision: yield* Schema.decodeUnknownEffect(AuthenticationRevision)({\n subjectId,\n securityRevision: row[s.securityRevision],\n credentials: credentials.sort((a, b) => a.credentialId.localeCompare(b.credentialId)),\n }),\n requirement: yield* s.decodeRequirement(row),\n now: yield* Schema.decodeEffect(Schema.Int)(Number(instant)),\n };\n });\n\n const read = Effect.fnUntraced(function* (subjectId: SubjectId, locking = false) {\n const native = yield* mapping.subjectId.toNative(subjectId);\n\n sessionInvariant((yield* mapping.subjectId.toSubject(native)) === subjectId);\n if (locking && nativeLocks) {\n const owners =\n yield* sql`select ${subject.fields(\"s_\")}, ${now} as engine_now from ${subject.name} where ${id(subject, s.id, native)} and ${activeSubject(subject)} limit 2 ${lock}`;\n\n sessionInvariant(owners.length <= 1);\n if (owners[0] === undefined) return undefined;\n\n const factors =\n yield* sql`select ${credential.fields(\"c_\")} from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)} order by ${credential.column(c.credentialId)} limit 65 ${lock}`;\n\n return yield* decode(\n subjectId,\n native,\n subject.decode(owners[0], \"s_\"),\n factors.map((row) => credential.decode(row, \"c_\")),\n owners[0].engine_now,\n );\n }\n\n const rows =\n yield* sql`select ${joinedSubject.fields(\"s_\")}, ${joinedCredential.fields(\"c_\")}, ${now} as engine_now from ${joinedSubject.name} left join ${joinedCredential.name} on ${id(joinedCredential, c.subjectId, native)} and ${activeCredential(joinedCredential)} where ${id(joinedSubject, s.id, native)} and ${activeSubject(joinedSubject)} limit 65`;\n\n if (rows[0] === undefined) return undefined;\n\n return yield* decode(\n subjectId,\n native,\n joinedSubject.decode(rows[0], \"s_\"),\n rows.map((row) => joinedCredential.decode(row, \"c_\")),\n rows[0].engine_now,\n );\n });\n\n const policyCondition = (row: Record<string, unknown>, native: unknown): Fragment => {\n if (!batch) return sql`1 = 1`;\n sessionInvariant(s.requirementColumns !== undefined);\n\n return sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${sql.and(s.requirementColumns.map((key) => (row[key] === null ? sql`${subject.column(key)} is null` : typeof row[key] === \"string\" ? exact(subject, key, row[key]) : id(subject, key, row[key]))))})`;\n };\n\n const authorityCondition = (revision: AuthenticationRevision, native: unknown): Fragment =>\n sql.and([\n sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${exact(subject, s.securityRevision, revision.securityRevision)} and ${activeSubject(subject)})`,\n sql`(select count(*) from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)}) = ${revision.credentials.length}`,\n ...revision.credentials.map(\n (factor) =>\n sql`exists(select 1 from ${credential.name} where ${id(credential, c.subjectId, native)} and ${exact(credential, c.credentialId, factor.credentialId)} and ${exact(credential, c.revision, factor.revision)} and ${activeCredential(credential)})`,\n ),\n ]);\n\n return {\n sql,\n subject,\n credential,\n joinedSubject,\n joinedCredential,\n now,\n lock,\n nativeLocks,\n exact,\n id,\n activeSubject,\n activeCredential,\n requested,\n decode,\n read,\n authorityCondition,\n policyCondition,\n };\n});\n\n/** Evaluate freshness at the database sample. The final write also enforces the\n * earliest future boundary at which the same evidence would cease to satisfy. */\nexport const assessSessionAt = Effect.fnUntraced(function* (\n evidence: AuthenticationEvidence,\n requirement: AuthenticationRequirement,\n now: number,\n) {\n const result = yield* assessAuthentication(evidence, requirement, DateTime.makeUnsafe(now));\n\n return result;\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-native-state.mjs","names":[],"sources":["../../src/internal/session-native-state.ts"],"sourcesContent":["import { AuthenticationClock } from \"@yielded/auth/Operations\";\nimport { reportAuthFailure } from \"@yielded/auth/Persistence\";\nimport type { SubjectId } from \"@yielded/auth/Schema\";\nimport {\n type AuthenticationRequirement,\n AuthenticationRevision,\n SessionConflict,\n SessionInvalid,\n SessionUnavailable,\n SessionStepUpInvalid,\n PendingAuthenticationInvalid,\n StaleAuthentication,\n assessAuthentication,\n type AuthenticationEvidence,\n} from \"@yielded/auth/Sessions\";\nimport { Cause, Effect, Schema, DateTime } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport { fragment, parameter, type Fragment } from \"effect/sql/Statement\";\n\nimport { PersistenceMappingError } from \"./mapping-error\";\nimport type { SessionAuthorityTables, SessionExecution } from \"./models/session-model\";\nimport { nativeSqlAlias, type NativeSqlTables, type SqlTable } from \"./native-sql-table\";\nimport { anySqlCondition, exactSqlText } from \"./sql-change\";\nimport { guardSqlitePolicy, type SqlitePolicyValue } from \"./sqlite-policy-guard\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport const sessionUnavailable = () => SessionUnavailable.make({});\n\nexport const sessionInvariant: (condition: unknown) => asserts condition = (condition) => {\n if (!condition)\n throw PersistenceMappingError.make({ operation: \"decode\", cause: \"Invalid session authority\" });\n};\n\nexport type SessionDomainFailure =\n | SessionConflict\n | SessionInvalid\n | SessionUnavailable\n | SessionStepUpInvalid\n | PendingAuthenticationInvalid\n | StaleAuthentication;\n\nconst isDomainFailure = (error: unknown): error is SessionDomainFailure =>\n Schema.is(SessionConflict)(error) ||\n Schema.is(SessionInvalid)(error) ||\n Schema.is(SessionUnavailable)(error) ||\n Schema.is(SessionStepUpInvalid)(error) ||\n Schema.is(PendingAuthenticationInvalid)(error) ||\n Schema.is(StaleAuthentication)(error);\n\n/** Only schema-checked domain failures cross the owner boundary; SQL/mapping\n * failures remain Unavailable. The executor preserves every mixed cause reason. */\nexport const sessionFailure = <E>(\n error: E,\n): Extract<E, SessionDomainFailure> | SessionUnavailable =>\n isDomainFailure(error) ? (error as Extract<E, SessionDomainFailure>) : sessionUnavailable();\n\nexport const normalizeSessionOperation = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n effect.pipe(\n Effect.tapError((error) =>\n isDomainFailure(error)\n ? Effect.void\n : reportAuthFailure(\"auth-persistence\", Cause.fail(error)),\n ),\n Effect.mapError(sessionFailure),\n );\n\nexport type NativeSessionAuthorityMapping = SessionAuthorityTables<\n AnyTableModel,\n AnyTableModel,\n unknown\n> &\n SessionExecution;\n\nexport interface NativeSessionAuthority {\n readonly native: unknown;\n readonly row: Record<string, unknown>;\n readonly policyValues: ReadonlyArray<SqlitePolicyValue>;\n readonly revision: AuthenticationRevision;\n readonly requirement: AuthenticationRequirement;\n readonly now: number;\n}\n\nexport const sameSessionRevision = (left: AuthenticationRevision, right: AuthenticationRevision) =>\n left.subjectId === right.subjectId &&\n left.securityRevision === right.securityRevision &&\n left.credentials.length === right.credentials.length &&\n new Set(left.credentials.map((f) => f.credentialId)).size === left.credentials.length &&\n left.credentials.every((f) =>\n right.credentials.some(\n (other) => other.credentialId === f.credentialId && other.revision === f.revision,\n ),\n );\n\n/** Known-subject joins bind each physical ID column independently. Subject locks\n * precede every credential lock, including inside an application coordinator. */\nexport const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (\n tables: NativeSqlTables,\n mapping: NativeSessionAuthorityMapping,\n batch = false,\n) {\n const sql = (yield* SqlClient).withoutTransforms();\n const clockPolicy = yield* AuthenticationClock;\n\n const s = mapping.subject,\n c = mapping.credential;\n\n if (batch) sessionInvariant(s.requirementColumns !== undefined);\n\n const subject = tables(s.table),\n credential = tables(c.table);\n\n const joinedSubject = subject.as(\"session_subject\"),\n joinedCredential = credential.as(\"session_factor\");\n\n const now = tables.expression(mapping.clock.engineNowMillis);\n const lock = sql.onDialectOrElse({ sqlite: () => sql``, orElse: () => sql`for update` });\n const nativeLocks = !batch && sql.onDialectOrElse({ sqlite: () => false, orElse: () => true });\n\n const exact = (table: SqlTable, key: string, value: unknown) =>\n exactSqlText(sql, table.column(key), table.value(key, value));\n\n const id = (table: SqlTable, key: string, value: unknown) =>\n sql`${table.column(key)} = ${table.value(key, value)}`;\n\n const activeSubject = (table: SqlTable) => id(table, s.status, s.activeStatusValue);\n\n const activeCredential = (table: SqlTable) => {\n if (c.status === undefined) return sql`1 = 1`;\n sessionInvariant(c.activeStatusValue !== undefined);\n\n return id(table, c.status, c.activeStatusValue);\n };\n\n const requested = (table: SqlTable, ids: ReadonlyArray<string>) =>\n anySqlCondition(ids.map((value) => exact(table, c.credentialId, value)));\n\n const decode = Effect.fnUntraced(function* (\n subjectId: SubjectId,\n native: unknown,\n projected: Record<string, unknown>,\n factors: ReadonlyArray<Record<string, unknown>>,\n instant: unknown,\n ): Effect.fn.Return<\n NativeSessionAuthority | undefined,\n PersistenceMappingError | Schema.SchemaError\n > {\n const row = subject.decode(projected, \"s_\");\n\n sessionInvariant((yield* mapping.subjectId.toSubject(row[s.id])) === subjectId);\n if (!s.isActiveStatus(row[s.status])) return undefined;\n const credentials: { credentialId: string; revision: string }[] = [];\n\n for (const factor of factors) {\n if (factor[c.credentialId] === null || factor[c.credentialId] === undefined) continue;\n sessionInvariant((yield* mapping.subjectId.toSubject(factor[c.subjectId])) === subjectId);\n if (c.status !== undefined) sessionInvariant(c.isActiveStatus?.(factor[c.status]) === true);\n\n const value = yield* Schema.decodeUnknownEffect(\n Schema.Struct({ credentialId: Schema.String, revision: Schema.String }),\n )({ credentialId: factor[c.credentialId], revision: factor[c.revision] });\n\n sessionInvariant(!credentials.some((other) => other.credentialId === value.credentialId));\n credentials.push(value);\n }\n sessionInvariant(credentials.length <= 64);\n\n return {\n native,\n row,\n // Compare the selected SQL representation, without round-tripping through\n // application codecs (for example, JSON whitespace or date precision).\n policyValues: batch\n ? (s.requirementColumns ?? []).map((key) => ({\n column: subject.selectedColumn(key),\n value: fragment([\n parameter(projected[nativeSqlAlias(\"s_\", subject.keys.indexOf(key))]),\n ]),\n }))\n : [],\n revision: yield* Schema.decodeUnknownEffect(AuthenticationRevision)({\n subjectId,\n securityRevision: row[s.securityRevision],\n credentials: credentials.sort((a, b) => a.credentialId.localeCompare(b.credentialId)),\n }),\n requirement: yield* s.decodeRequirement(row),\n now: yield* Schema.decodeEffect(Schema.Int)(Number(instant)),\n };\n });\n\n const read = Effect.fnUntraced(function* (subjectId: SubjectId, locking = false) {\n const native = yield* mapping.subjectId.toNative(subjectId);\n\n sessionInvariant((yield* mapping.subjectId.toSubject(native)) === subjectId);\n if (locking && nativeLocks) {\n const owners =\n yield* sql`select ${subject.fields(\"s_\")}, ${now} as engine_now from ${subject.name} where ${id(subject, s.id, native)} and ${activeSubject(subject)} limit 2 ${lock}`;\n\n sessionInvariant(owners.length <= 1);\n if (owners[0] === undefined) return undefined;\n\n const factors =\n yield* sql`select ${credential.fields(\"c_\")} from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)} order by ${credential.column(c.credentialId)} limit 65 ${lock}`;\n\n return yield* decode(\n subjectId,\n native,\n owners[0],\n factors.map((row) => credential.decode(row, \"c_\")),\n owners[0].engine_now,\n );\n }\n\n const rows =\n yield* sql`select ${joinedSubject.fields(\"s_\")}, ${joinedCredential.fields(\"c_\")}, ${now} as engine_now from ${joinedSubject.name} left join ${joinedCredential.name} on ${id(joinedCredential, c.subjectId, native)} and ${activeCredential(joinedCredential)} where ${id(joinedSubject, s.id, native)} and ${activeSubject(joinedSubject)} limit 65`;\n\n if (rows[0] === undefined) return undefined;\n\n return yield* decode(\n subjectId,\n native,\n rows[0],\n rows.map((row) => joinedCredential.decode(row, \"c_\")),\n rows[0].engine_now,\n );\n });\n\n const guardPolicy = Effect.fnUntraced(function* (current: NativeSessionAuthority) {\n if (!batch) return;\n sessionInvariant(s.requirementColumns !== undefined);\n\n yield* guardSqlitePolicy({\n name: \"session-subject-policy\",\n table: subject.name,\n owner: id(subject, s.id, current.native),\n values: current.policyValues,\n });\n });\n\n const authorityCondition = (revision: AuthenticationRevision, native: unknown): Fragment =>\n sql.and([\n sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${exact(subject, s.securityRevision, revision.securityRevision)} and ${activeSubject(subject)})`,\n sql`(select count(*) from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)}) = ${revision.credentials.length}`,\n ...revision.credentials.map(\n (factor) =>\n sql`exists(select 1 from ${credential.name} where ${id(credential, c.subjectId, native)} and ${exact(credential, c.credentialId, factor.credentialId)} and ${exact(credential, c.revision, factor.revision)} and ${activeCredential(credential)})`,\n ),\n ]);\n\n return {\n sql,\n clockPolicy,\n subject,\n credential,\n joinedSubject,\n joinedCredential,\n now,\n lock,\n nativeLocks,\n exact,\n id,\n activeSubject,\n activeCredential,\n requested,\n decode,\n read,\n authorityCondition,\n guardPolicy,\n };\n});\n\n/** Evaluate freshness at the database sample. The final write also enforces the\n * earliest future boundary at which the same evidence would cease to satisfy. */\nexport const assessSessionAt = Effect.fnUntraced(function* (\n evidence: AuthenticationEvidence,\n requirement: AuthenticationRequirement,\n now: number,\n) {\n const result = yield* assessAuthentication(evidence, requirement, DateTime.makeUnsafe(now));\n\n return result;\n});\n"],"mappings":"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"}
|
|
@@ -4,13 +4,13 @@ import { NativeSqlTables } from "./native-sql-table.mjs";
|
|
|
4
4
|
import { SqlBatchCommit } from "./sql-commit.mjs";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
6
|
import { LifecycleHooks } from "@yielded/auth/Hooks";
|
|
7
|
-
import { SessionRepository, StatefulSessionPersistence } from "@yielded/auth/Sessions";
|
|
7
|
+
import { SessionRepository, SessionUnavailable, StatefulSessionPersistence } from "@yielded/auth/Sessions";
|
|
8
8
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
9
9
|
//#region src/internal/session-native-stateful.d.ts
|
|
10
10
|
export type NativeStatefulSessionMapping<Claims> = StatefulSessionMapping<Claims, AnyTableModel, AnyTableModel, AnyTableModel, AnyTableModel, unknown, unknown>;
|
|
11
11
|
export declare const makeNativeStatefulSessionServices: <Claims>(tables: NativeSqlTables, mapping: NativeStatefulSessionMapping<Claims>) => Effect.Effect<{
|
|
12
12
|
readonly statefulSessionPersistence: StatefulSessionPersistence<Claims>;
|
|
13
13
|
readonly sessionRepository: SessionRepository;
|
|
14
|
-
},
|
|
14
|
+
}, SessionUnavailable, LifecycleHooks | SqlBatchCommit | SqlClient>;
|
|
15
15
|
//#endregion
|
|
16
16
|
//# sourceMappingURL=session-native-stateful.d.mts.map
|
|
@@ -4,11 +4,13 @@ import { exactSqlText, executeSqlChange } from "./sql-change.mjs";
|
|
|
4
4
|
import { CurrentSqlCommit, SqlBatchCommit, appendSqlBatchStatement, makeSqlCommitExecutor, registerSqlBatchPostcondition, registerSqlPostcondition } from "./sql-commit.mjs";
|
|
5
5
|
import { makeNativeSessionAuthorityState, normalizeSessionOperation, sameSessionRevision, sessionInvariant, sessionUnavailable } from "./session-native-state.mjs";
|
|
6
6
|
import { makeConditionalSqlInsert, makeNativeSessionRecords, sameSessionRecord } from "./session-native-record.mjs";
|
|
7
|
+
import { canJoinTextColumns, withStorageValidation } from "./storage-validation.mjs";
|
|
7
8
|
import { makeNativeSessionPending, prepareNativeSession } from "./session-native-pending.mjs";
|
|
8
9
|
import { allocateSessionValue, preservesRevision } from "./session-policy.mjs";
|
|
9
10
|
import { sessionEvidenceDeadline } from "./session-native-authority.mjs";
|
|
10
11
|
import { DateTime, Effect, Option, Schema } from "effect";
|
|
11
12
|
import { PendingAuthenticationInvalid, SessionConflict, SessionId, SessionInvalid, SessionMetadata, StaleAuthentication, snapshotSessionAuthenticationProvenance } from "@yielded/auth/Sessions";
|
|
13
|
+
import { AuthenticationClock } from "@yielded/auth/Operations";
|
|
12
14
|
//#region src/internal/session-native-stateful.ts
|
|
13
15
|
const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, mapping) {
|
|
14
16
|
const batch = yield* SqlBatchCommit;
|
|
@@ -22,7 +24,7 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
22
24
|
...mapping.pending
|
|
23
25
|
}, batch !== void 0);
|
|
24
26
|
const { sql, s, table, now, exact, live } = records;
|
|
25
|
-
const native = (work, mode = "transaction") => batch === void 0 ? executor.operation(normalizeSessionOperation(work), mode) : executor.operationBatch(normalizeSessionOperation(work)).pipe(Effect.provideService(SqlBatchCommit, batch));
|
|
27
|
+
const native = (work, mode = "transaction") => (batch === void 0 ? executor.operation(normalizeSessionOperation(work), mode) : executor.operationBatch(normalizeSessionOperation(work)).pipe(Effect.provideService(SqlBatchCommit, batch))).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy));
|
|
26
28
|
const stage = Effect.fnUntraced(function* (statement, expected) {
|
|
27
29
|
if (batch !== void 0) {
|
|
28
30
|
yield* appendSqlBatchStatement(sql`${statement}`);
|
|
@@ -41,7 +43,7 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
41
43
|
check: Effect.gen(function* () {
|
|
42
44
|
const rows = yield* sql`select 1 where ${condition}`;
|
|
43
45
|
sessionInvariant(rows.length === 1);
|
|
44
|
-
}).pipe(Effect.mapError((cause) => PersistenceMappingError.make({
|
|
46
|
+
}).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy), Effect.mapError((cause) => PersistenceMappingError.make({
|
|
45
47
|
operation: "decode",
|
|
46
48
|
cause
|
|
47
49
|
})))
|
|
@@ -51,25 +53,26 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
51
53
|
return sql`exists(select 1 from ${table.name} where ${yield* records.owner(record)} and ${exact(table, s.securityRevision, record.securityRevision)} and ${records.millis(table, s.expiresAt)} = ${DateTime.toEpochMillis(record.expiresAt)} and ${records.millis(table, s.absoluteExpiresAt)} = ${DateTime.toEpochMillis(record.absoluteExpiresAt)} and ${records.encodedCondition(values)} and ${live(table)})`;
|
|
52
54
|
});
|
|
53
55
|
const classify = (effect) => effect.pipe(Effect.mapError((error) => isMappedConstraintConflict(mapping.isConstraintConflict, error) ? SessionConflict.make({}) : error));
|
|
56
|
+
const [joinedIds, joinedRevisions] = yield* Effect.forEach([[table.unencodedTextColumn?.(s.subjectId), state.subject.unencodedTextColumn?.(mapping.subject.id)], [table.unencodedTextColumn?.(s.securityRevision), state.subject.unencodedTextColumn?.(mapping.subject.securityRevision)]], canJoinTextColumns).pipe(withStorageValidation, Effect.mapError(sessionUnavailable));
|
|
54
57
|
const verify = Effect.fnUntraced(function* (digest) {
|
|
55
58
|
const subject = state.subject.as("verified_subject"), session = table.as("verified_session"), owner = mapping.subject;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return record;
|
|
59
|
+
let discoveredSubjectId;
|
|
60
|
+
let nativeSubject;
|
|
61
|
+
if (!joinedIds) {
|
|
62
|
+
const discovered = yield* records.read(digest);
|
|
63
|
+
if (discovered === void 0) return void 0;
|
|
64
|
+
discoveredSubjectId = discovered.subjectId;
|
|
65
|
+
nativeSubject = yield* mapping.subjectId.toNative(discoveredSubjectId);
|
|
64
66
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const rows = yield* sql`select ${subject.fields("subject_")} from ${subject.name} where ${state.id(subject, owner.id, nativeSubject)} and ${state.activeSubject(subject)} and ${state.exact(subject, owner.securityRevision, record.securityRevision)} and ${now} < ${DateTime.toEpochMillis(record.expiresAt)} limit 2`;
|
|
67
|
+
const owns = joinedIds ? exactSqlText(sql, subject.column(owner.id), session.column(s.subjectId)) : sql`${state.id(subject, owner.id, nativeSubject)} and ${state.id(session, s.subjectId, nativeSubject)}`;
|
|
68
|
+
const revision = joinedRevisions ? exactSqlText(sql, subject.column(owner.securityRevision), session.column(s.securityRevision)) : sql`true`;
|
|
69
|
+
const rows = yield* sql`select ${session.fields("session_")}, ${subject.fields("subject_")} from ${session.name} join ${subject.name} on ${owns} and ${state.activeSubject(subject)} and ${revision} where ${exact(session, s.digest, digest)} and ${live(session)} limit 2`;
|
|
69
70
|
sessionInvariant(rows.length <= 1);
|
|
70
71
|
if (rows[0] === void 0) return void 0;
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const record = yield* records.decode(session.decode(rows[0], "session_"));
|
|
73
|
+
const decoded = subject.decode(rows[0], "subject_");
|
|
74
|
+
sessionInvariant((joinedIds || record.subjectId === discoveredSubjectId) && (yield* mapping.subjectId.toSubject(decoded[owner.id])) === record.subjectId && owner.isActiveStatus(decoded[owner.status]));
|
|
75
|
+
return decoded[owner.securityRevision] === record.securityRevision ? record : void 0;
|
|
73
76
|
});
|
|
74
77
|
return {
|
|
75
78
|
statefulSessionPersistence: {
|
|
@@ -77,8 +80,8 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
77
80
|
const current = yield* state.read(input.evidence.revision.subjectId, true);
|
|
78
81
|
if (current === void 0 || !sameSessionRevision(current.revision, input.evidence.revision)) return yield* StaleAuthentication.make({});
|
|
79
82
|
const assessment = yield* sessionEvidenceDeadline(input.evidence, current.requirement, current.now);
|
|
80
|
-
const expires = DateTime.toEpochMillis(input.session.expiresAt), absolute = DateTime.toEpochMillis(input.session.absoluteExpiresAt);
|
|
81
|
-
if (current.now >= expires || expires > absolute) return yield* StaleAuthentication.make({});
|
|
83
|
+
const issued = Math.max(current.now, DateTime.toEpochMillis(input.now), DateTime.toEpochMillis(input.session.issuedAt), ...input.evidence.proofs.map((proof) => DateTime.toEpochMillis(proof.verifiedAt))), expires = DateTime.toEpochMillis(input.session.expiresAt), absolute = DateTime.toEpochMillis(input.session.absoluteExpiresAt);
|
|
84
|
+
if (issued - current.now > state.clockPolicy.futureToleranceMillis || issued >= expires || current.now >= expires || expires > absolute) return yield* StaleAuthentication.make({});
|
|
82
85
|
let pendingExpiry = Infinity;
|
|
83
86
|
if (input.pending !== void 0) {
|
|
84
87
|
if (pending === void 0 || mapping.pending === void 0) return yield* PendingAuthenticationInvalid.make({});
|
|
@@ -97,12 +100,12 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
97
100
|
sessionId,
|
|
98
101
|
subjectId: current.revision.subjectId,
|
|
99
102
|
securityRevision: current.revision.securityRevision,
|
|
100
|
-
assurance: input.handoffSourceSessionId === void 0 ? assessment.assessed.assurance : input.session.assurance,
|
|
101
103
|
provenance: yield* snapshotSessionAuthenticationProvenance({ evidence: input.evidence }),
|
|
102
|
-
issuedAt: DateTime.makeUnsafe(
|
|
104
|
+
issuedAt: DateTime.makeUnsafe(issued)
|
|
103
105
|
};
|
|
104
106
|
const deadline = Math.min(expires, assessment.validUntil, pendingExpiry);
|
|
105
|
-
|
|
107
|
+
yield* state.guardPolicy(current);
|
|
108
|
+
const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${deadline}`;
|
|
106
109
|
const values = {
|
|
107
110
|
...s.encodeInsert(record, {
|
|
108
111
|
subjectId: current.native,
|
|
@@ -127,7 +130,7 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
127
130
|
const authority = yield* state.read(record.subjectId);
|
|
128
131
|
sessionInvariant(authority !== void 0 && sameSessionRevision(authority.revision, input.evidence.revision));
|
|
129
132
|
yield* sessionEvidenceDeadline(input.evidence, authority.requirement, authority.now);
|
|
130
|
-
}).pipe(Effect.mapError((cause) => PersistenceMappingError.make({
|
|
133
|
+
}).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy), Effect.mapError((cause) => PersistenceMappingError.make({
|
|
131
134
|
operation: "decode",
|
|
132
135
|
cause
|
|
133
136
|
})))
|
|
@@ -136,8 +139,8 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
136
139
|
}))),
|
|
137
140
|
verify: ({ digest }) => executor.verify(verify(digest)).pipe(Effect.flatMap((record) => record === void 0 ? Effect.fail(SessionInvalid.make({})) : Effect.succeed(record))),
|
|
138
141
|
rotate: (input, project) => native(classify(Effect.gen(function* () {
|
|
139
|
-
const absolute = DateTime.toEpochMillis(input.record.absoluteExpiresAt), expires = DateTime.toEpochMillis(input.nextExpiresAt);
|
|
140
|
-
if (expires > absolute || input.nextDigest === input.record.digest || input.nextCredentialVersion === input.record.credentialVersion) return yield* SessionConflict.make({});
|
|
142
|
+
const absolute = DateTime.toEpochMillis(input.record.absoluteExpiresAt), expires = DateTime.toEpochMillis(input.nextExpiresAt), preparedAt = Math.max(DateTime.toEpochMillis(input.now), DateTime.toEpochMillis(input.record.issuedAt));
|
|
143
|
+
if (expires > absolute || preparedAt >= expires || input.nextDigest === input.record.digest || input.nextCredentialVersion === input.record.credentialVersion) return yield* SessionConflict.make({});
|
|
141
144
|
const needsSample = batch !== void 0 || sql.onDialectOrElse({
|
|
142
145
|
mysql: () => true,
|
|
143
146
|
orElse: () => false
|
|
@@ -149,15 +152,15 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
149
152
|
digest: input.nextDigest,
|
|
150
153
|
credentialVersion: input.nextCredentialVersion,
|
|
151
154
|
expiresAt: input.nextExpiresAt,
|
|
152
|
-
issuedAt: DateTime.makeUnsafe(sampled)
|
|
155
|
+
issuedAt: DateTime.makeUnsafe(Math.max(sampled, preparedAt))
|
|
153
156
|
};
|
|
154
157
|
const values = {
|
|
155
158
|
...s.encodeRotation(prepared),
|
|
156
159
|
[s.digest]: prepared.digest,
|
|
157
|
-
[s.issuedAt]: tables.expression(mapping.clock.fromMillis(now)),
|
|
160
|
+
[s.issuedAt]: tables.expression(mapping.clock.fromMillis(sql`case when ${now} >= ${preparedAt} then ${now} else ${preparedAt} end`)),
|
|
158
161
|
[s.expiresAt]: s.encodeInstant(prepared.expiresAt)
|
|
159
162
|
};
|
|
160
|
-
const condition = sql`${yield* records.owner(input.record)} and ${live(table)} and ${now} < ${expires} and ${records.millis(table, s.absoluteExpiresAt)} = ${absolute}`;
|
|
163
|
+
const condition = sql`${yield* records.owner(input.record)} and ${live(table)} and ${preparedAt} <= ${now} + ${state.clockPolicy.futureToleranceMillis} and ${now} < ${expires} and ${records.millis(table, s.absoluteExpiresAt)} = ${absolute}`;
|
|
161
164
|
const update = sql`${table.update(values)} where ${condition}`;
|
|
162
165
|
let record = prepared;
|
|
163
166
|
if (needsSample) {
|
|
@@ -205,17 +208,43 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
|
|
|
205
208
|
sessionInvariant(Number.isSafeInteger(input.limit) && input.limit > 0 && input.limit <= 1e3);
|
|
206
209
|
const nativeSubject = yield* mapping.subjectId.toNative(input.subjectId), st = state.subject.as("listed_subject"), list = table.as("listed_session"), owner = mapping.subject;
|
|
207
210
|
const cursor = input.cursor === void 0 ? void 0 : yield* mapping.sessionId.toNative(yield* Schema.decodeEffect(SessionId)(input.cursor));
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
sessionInvariant(
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
211
|
+
let revision;
|
|
212
|
+
if (!joinedRevisions) {
|
|
213
|
+
const subjects = yield* sql`select ${st.fields("subject_")} from ${st.name} where ${state.id(st, owner.id, nativeSubject)} and ${state.activeSubject(st)} limit 2`;
|
|
214
|
+
sessionInvariant(subjects.length <= 1);
|
|
215
|
+
if (subjects[0] === void 0) return { sessions: [] };
|
|
216
|
+
const subject = st.decode(subjects[0], "subject_");
|
|
217
|
+
sessionInvariant((yield* mapping.subjectId.toSubject(subject[owner.id])) === input.subjectId);
|
|
218
|
+
revision = yield* Schema.decodeUnknownEffect(SessionMetadata.fields.securityRevision)(subject[owner.securityRevision]);
|
|
219
|
+
}
|
|
220
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
221
|
+
const currentRevision = joinedRevisions ? exactSqlText(sql, st.column(owner.securityRevision), list.column(s.securityRevision)) : sql`${state.exact(st, owner.securityRevision, revision)} and ${exact(list, s.securityRevision, revision)}`;
|
|
222
|
+
const rows = yield* sql`select ${list.fields("session_")}, ${st.fields("subject_")}, ${list.column(s.subjectId)} as session_owner from ${st.name} left join ${list.name} on ${state.id(list, s.subjectId, nativeSubject)} and ${currentRevision} and ${live(list)}${cursor === void 0 ? sql`` : sql` and ${list.column(s.sessionId)} > ${list.value(s.sessionId, cursor)}`} where ${state.id(st, owner.id, nativeSubject)} and ${state.activeSubject(st)} order by ${list.column(s.sessionId)} limit ${input.limit + 1}`;
|
|
223
|
+
const first = rows[0];
|
|
224
|
+
if (first === void 0) return { sessions: [] };
|
|
225
|
+
if (!joinedRevisions) {
|
|
226
|
+
const subject = st.decode(first, "subject_");
|
|
227
|
+
sessionInvariant((yield* mapping.subjectId.toSubject(subject[owner.id])) === input.subjectId);
|
|
228
|
+
const current = yield* Schema.decodeUnknownEffect(SessionMetadata.fields.securityRevision)(subject[owner.securityRevision]);
|
|
229
|
+
if (current !== revision) {
|
|
230
|
+
revision = current;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (first.session_owner === null) return { sessions: [] };
|
|
235
|
+
const decoded = yield* Effect.forEach(rows.slice(0, input.limit), (row) => Effect.gen(function* () {
|
|
236
|
+
const record = yield* records.decode(list.decode(row, "session_"));
|
|
237
|
+
const subject = st.decode(row, "subject_");
|
|
238
|
+
sessionInvariant(record.subjectId === input.subjectId && (yield* mapping.subjectId.toSubject(subject[owner.id])) === input.subjectId && subject[owner.securityRevision] === record.securityRevision);
|
|
239
|
+
return yield* Schema.decodeEffect(Schema.toType(SessionMetadata))(record);
|
|
240
|
+
}));
|
|
241
|
+
const nextCursor = rows.length > input.limit ? decoded.at(-1)?.sessionId : void 0;
|
|
242
|
+
return {
|
|
243
|
+
sessions: decoded,
|
|
244
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return yield* sessionUnavailable();
|
|
219
248
|
})) }
|
|
220
249
|
};
|
|
221
250
|
});
|