@yielded/auth-persistence 0.1.0-beta.29 → 0.1.0-beta.30

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 CHANGED
@@ -37,3 +37,6 @@ Plain reads use no transaction. Protected mutations retain their named final che
37
37
  when an application joins additional work to the owner.
38
38
  See [persistence examples](../../docs/src/content/docs/reference/adapters.md#runnable-examples) for all
39
39
  four ownership models and their current limits.
40
+
41
+ The opt-in [`Testing` module](../../docs/src/content/docs/guide/storage.mdx#in-memory-tests)
42
+ provides non-durable password and session storage with portable crypto services for tests.
@@ -0,0 +1,44 @@
1
+ import { ClaimsCodec, Definition, PersistenceConfigurationError } from "./internal/configuration.mjs";
2
+ import { Context, Crypto, Layer, Schema } from "effect";
3
+ import { PasswordHashing, PasswordPersistence } from "@yielded/auth/Password";
4
+ import { AuthenticationAuthority, AuthenticationRequirement } from "@yielded/auth/Sessions";
5
+ //#region src/Testing.d.ts
6
+ /** One existing subject and one byte-preserving password. Seeding does not verify
7
+ * an email or exercise registration/screening. Passwords are hashed on acquisition
8
+ * by the supplied PasswordHashing service; they are never stored as plaintext. */
9
+ export declare const PasswordSubject: Schema.Struct<{
10
+ readonly subjectId: Schema.brand<Schema.NonEmptyString, "effect-auth/SubjectId">;
11
+ readonly email: Schema.decodeTo<Schema.brand<Schema.String, "effect-auth/Email">, Schema.String, never, never>;
12
+ readonly password: Schema.Redacted<Schema.String>;
13
+ readonly active: Schema.optionalKey<Schema.Boolean>;
14
+ }>;
15
+ export interface Options {
16
+ readonly subjects: ReadonlyArray<typeof PasswordSubject.Encoded>;
17
+ /** Application policy; no permissive test default. */
18
+ readonly requirement: AuthenticationRequirement;
19
+ }
20
+ export interface ServicesOptions {
21
+ /** Reproducible test entropy. Omit to use the host's secure randomness. */
22
+ readonly seed?: string | number;
23
+ }
24
+ /** Portable password hashing and Effect Crypto for tests. Requires global WebCrypto
25
+ * at acquisition; uses the default password cost and one shared KDF admission owner.
26
+ * A seed restarts its private random sequence on each acquisition; it never changes
27
+ * the caller's Random or Clock services. Seeded credentials are only suitable for tests.
28
+ * Provide this after composing Auth with Testing.layer and application claims.
29
+ */
30
+ export declare const services: (options?: ServicesOptions) => Layer.Layer<Crypto.Crypto | PasswordHashing, import("@yielded/crypto/Errors").InvalidInput | import("@yielded/auth/Password").PasswordConfigurationError | PersistenceConfigurationError, never>;
31
+ /** Fresh scoped memory for exactly one Password.make() sign-in strategy and
32
+ * stateful sessions: issue, verify, renew, list, revoke and sign out. No SQL,
33
+ * filesystem, native module, platform detection or implicit cryptography.
34
+ * Uses the Effect Clock captured at acquisition, including TestClock.
35
+ * Claims, hashing, Crypto, session policy and private delivery remain application
36
+ * responsibilities. Other strategies, password management and session modes fail
37
+ * at acquisition; pending authentication, handoff and ambient commits are unsupported.
38
+ * Separate acquisitions own separate state, cleared on scope close. Reusing this
39
+ * Layer within one build shares state. Never use for production authority or to
40
+ * verify a production adapter's concurrency, durability or crash recovery.
41
+ */
42
+ export declare const layer: <C extends ClaimsCodec, const Id extends string>(auth: Definition<C, Id>, options: Options) => Layer.Layer<AuthenticationAuthority | PasswordPersistence | Context.Service.Identifier<Definition<C, Id>["sessions"]["StatefulSessionPersistence"]> | Context.Service.Identifier<Definition<C, Id>["sessions"]["SessionRepository"]>, PersistenceConfigurationError, PasswordHashing>;
43
+ //#endregion
44
+ //# sourceMappingURL=Testing.d.mts.map
@@ -0,0 +1,310 @@
1
+ import { PersistenceConfigurationError } from "./internal/configuration.mjs";
2
+ import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Random, Redacted, Schema, Semaphore } from "effect";
3
+ import { LifecycleHooks, coordinateCommit, hasCommitScope } from "@yielded/auth/Hooks";
4
+ import { EncodedPasswordHash, PasswordCredentialSnapshot, PasswordHashing, PasswordPersistence, PasswordUnavailable, snapshotPasswordCredential, snapshotPasswordRequirement, snapshotPasswordRevision } from "@yielded/auth/Password";
5
+ import { hooksLayer } from "@yielded/auth/Persistence";
6
+ import { AuthenticationAuthority, PendingAuthenticationInvalid, SecurityRevision, SessionAuthenticationProvenance, SessionConflict, SessionCredentialVersion, SessionId, SessionInvalid, SessionMetadata, SessionUnavailable, StaleAuthentication, assessAuthentication, snapshotAuthenticationEvidence, snapshotSessionAuthenticationProvenance } from "@yielded/auth/Sessions";
7
+ import { Email, SubjectId, TokenDigest } from "@yielded/auth/Schema";
8
+ import * as KdfAdmission from "@yielded/crypto/KdfAdmission";
9
+ import * as Portable from "@yielded/crypto/Portable";
10
+ import { layerCryptoWeb } from "@yielded/crypto/WebCrypto";
11
+ //#region src/Testing.ts
12
+ /** Explicit, runtime-independent test support. Never imported by production entrypoints. */
13
+ /** One existing subject and one byte-preserving password. Seeding does not verify
14
+ * an email or exercise registration/screening. Passwords are hashed on acquisition
15
+ * by the supplied PasswordHashing service; they are never stored as plaintext. */
16
+ const PasswordSubject = Schema.Struct({
17
+ subjectId: SubjectId,
18
+ email: Email,
19
+ password: Schema.Redacted(Schema.String),
20
+ active: Schema.optionalKey(Schema.Boolean)
21
+ });
22
+ const configurationError = (reason) => PersistenceConfigurationError.make({ reason });
23
+ const unavailable = () => SessionUnavailable.make({});
24
+ /** Portable password hashing and Effect Crypto for tests. Requires global WebCrypto
25
+ * at acquisition; uses the default password cost and one shared KDF admission owner.
26
+ * A seed restarts its private random sequence on each acquisition; it never changes
27
+ * the caller's Random or Clock services. Seeded credentials are only suitable for tests.
28
+ * Provide this after composing Auth with Testing.layer and application claims.
29
+ */
30
+ const services = (options = {}) => {
31
+ const { seed } = options;
32
+ return Layer.unwrap(Effect.gen(function* () {
33
+ const subtle = globalThis.crypto?.subtle;
34
+ if (subtle === void 0) return yield* configurationError("Testing.services requires global WebCrypto");
35
+ const entropy = seed === void 0 ? layerCryptoWeb : Layer.effect(Crypto.Crypto, Effect.gen(function* () {
36
+ const native = yield* Crypto.Crypto;
37
+ const random = yield* Random.Random.pipe(Random.withSeed(seed));
38
+ return Crypto.make({
39
+ randomBytes: (size) => Uint8Array.from({ length: size }, () => random.nextIntUnsafe() & 255),
40
+ digest: (algorithm, data) => native.digest(algorithm, data)
41
+ });
42
+ })).pipe(Layer.provide(layerCryptoWeb));
43
+ const backend = Portable.layer(subtle).pipe(Layer.provideMerge(KdfAdmission.layer()));
44
+ return PasswordHashing.layer().pipe(Layer.provide(backend), Layer.provideMerge(entropy));
45
+ }));
46
+ };
47
+ /** Fresh scoped memory for exactly one Password.make() sign-in strategy and
48
+ * stateful sessions: issue, verify, renew, list, revoke and sign out. No SQL,
49
+ * filesystem, native module, platform detection or implicit cryptography.
50
+ * Uses the Effect Clock captured at acquisition, including TestClock.
51
+ * Claims, hashing, Crypto, session policy and private delivery remain application
52
+ * responsibilities. Other strategies, password management and session modes fail
53
+ * at acquisition; pending authentication, handoff and ambient commits are unsupported.
54
+ * Separate acquisitions own separate state, cleared on scope close. Reusing this
55
+ * Layer within one build shares state. Never use for production authority or to
56
+ * verify a production adapter's concurrency, durability or crash recovery.
57
+ */
58
+ const layer = (auth, options) => Layer.effectContext(Effect.gen(function* () {
59
+ const strategies = Object.values(auth.strategies);
60
+ const feature = strategies[0]?.persistence;
61
+ if (auth.sessionMode !== "stateful" || strategies.length !== 1 || feature?.kind !== "password" || feature.management || feature.lifecycle) return yield* configurationError("Testing.layer supports one sign-in-only Password.make() strategy and stateful sessions");
62
+ const seeds = yield* Schema.decodeEffect(Schema.Array(PasswordSubject))(options.subjects).pipe(Effect.mapError(() => configurationError("Invalid in-memory password subjects")));
63
+ const requirement = yield* snapshotPasswordRequirement(options.requirement).pipe(Effect.mapError(() => configurationError("Invalid in-memory authentication requirement")));
64
+ const hasher = yield* PasswordHashing;
65
+ const clock = yield* Clock.Clock;
66
+ const hooks = yield* LifecycleHooks;
67
+ const mutex = yield* Semaphore.make(1);
68
+ const claims = auth.claims;
69
+ const Session = Schema.Struct({
70
+ ...SessionMetadata.fields,
71
+ claims,
72
+ digest: TokenDigest,
73
+ provenance: SessionAuthenticationProvenance,
74
+ credentialVersion: SessionCredentialVersion
75
+ });
76
+ const Database = Schema.Struct({
77
+ sequence: Schema.Natural,
78
+ subjects: Schema.Array(Schema.Struct({
79
+ active: Schema.Boolean,
80
+ credential: PasswordCredentialSnapshot
81
+ })),
82
+ sessions: Schema.Array(Session)
83
+ });
84
+ const codec = Schema.fromJsonString(Database);
85
+ const sessionCodec = Schema.fromJsonString(Session);
86
+ const credentialCodec = Schema.fromJsonString(PasswordCredentialSnapshot);
87
+ const initial = {
88
+ sequence: 0,
89
+ subjects: [],
90
+ sessions: []
91
+ };
92
+ const ids = /* @__PURE__ */ new Set();
93
+ const emails = /* @__PURE__ */ new Set();
94
+ for (const seed of seeds) {
95
+ if (ids.has(seed.subjectId) || emails.has(seed.email)) return yield* configurationError("Duplicate in-memory subject ID or email");
96
+ ids.add(seed.subjectId);
97
+ emails.add(seed.email);
98
+ const verifier = yield* hasher.hash(seed.password).pipe(Effect.mapError(() => configurationError("Cannot hash in-memory password seed")));
99
+ const credentialId = `password:${seed.subjectId}`;
100
+ const credential = yield* Schema.decodeEffect(PasswordCredentialSnapshot)({
101
+ moduleId: feature.moduleId,
102
+ revision: {
103
+ subjectId: seed.subjectId,
104
+ securityRevision: "1",
105
+ credentials: [{
106
+ credentialId,
107
+ revision: "1"
108
+ }]
109
+ },
110
+ credentialId,
111
+ credentialRevision: "1",
112
+ verifierVersion: "1",
113
+ verifier: Redacted.value(verifier),
114
+ normalization: "none",
115
+ identifier: {
116
+ namespace: "email",
117
+ value: seed.email
118
+ },
119
+ identifierBindingRevision: "1"
120
+ }).pipe(Effect.mapError(() => configurationError("Invalid in-memory password seed")));
121
+ initial.subjects = [...initial.subjects, {
122
+ active: seed.active ?? true,
123
+ credential
124
+ }];
125
+ }
126
+ let stored = yield* Schema.encodeEffect(codec)(initial).pipe(Effect.mapError(() => configurationError("Cannot initialize in-memory storage")));
127
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
128
+ stored = void 0;
129
+ }));
130
+ const load = Effect.suspend(() => stored === void 0 ? Effect.fail(unavailable()) : Schema.decodeEffect(codec)(stored).pipe(Effect.mapError(unavailable)));
131
+ const now = () => clock.currentTimeMillisUnsafe();
132
+ const nextId = (state) => `memory-${++state.sequence}`;
133
+ const subject = (state, id) => state.subjects.find((item) => item.active && item.credential.revision.subjectId === id);
134
+ const current = (state, revision) => {
135
+ const found = subject(state, revision.subjectId)?.credential;
136
+ return found !== void 0 && found.revision.securityRevision === revision.securityRevision && revision.credentials.length === 1 && revision.credentials[0]?.credentialId === found.credentialId && revision.credentials[0]?.revision === found.credentialRevision;
137
+ };
138
+ const live = (state, session, time) => subject(state, session.subjectId)?.credential.revision.securityRevision === session.securityRevision && time >= DateTime.toEpochMillis(session.issuedAt) && time < Math.min(DateTime.toEpochMillis(session.expiresAt), DateTime.toEpochMillis(session.absoluteExpiresAt));
139
+ const standalone = Effect.gen(function* () {
140
+ if (yield* hasCommitScope) return yield* unavailable();
141
+ });
142
+ const read = (body) => standalone.pipe(Effect.andThen(mutex.withPermits(1)(Effect.flatMap(load, (state) => body(state, now())))));
143
+ const transaction = (body) => standalone.pipe(Effect.andThen(coordinateCommit((journal) => mutex.withPermits(1)(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
144
+ const { receipt, deadline, time, encoded } = yield* restore(Effect.gen(function* () {
145
+ const state = yield* load;
146
+ const time = now();
147
+ const { receipt, deadline = Infinity } = yield* body(state, journal, time);
148
+ const encoded = yield* Schema.encodeEffect(codec)(state).pipe(Effect.mapError(unavailable));
149
+ const status = yield* Effect.result(receipt.read);
150
+ if (status._tag !== "Failure" || status.failure._tag !== "CommitPending") return yield* unavailable();
151
+ return {
152
+ receipt,
153
+ deadline,
154
+ time,
155
+ encoded
156
+ };
157
+ }));
158
+ yield* Effect.try({
159
+ try: () => {
160
+ const finalTime = now();
161
+ if (stored === void 0 || finalTime < time || finalTime >= deadline) throw unavailable();
162
+ stored = encoded;
163
+ },
164
+ catch: unavailable
165
+ });
166
+ return receipt;
167
+ }))))), Effect.map((result) => result.value), Effect.catchTag("HookConfigurationError", unavailable), Effect.provideService(LifecycleHooks, hooks));
168
+ const authority = AuthenticationAuthority.of({
169
+ capture: (id, anchors) => read((state) => Effect.gen(function* () {
170
+ const found = subject(state, id)?.credential;
171
+ if (found === void 0 || new Set(anchors).size !== anchors.length || anchors.some((anchor) => anchor !== found.credentialId)) return yield* StaleAuthentication.make({});
172
+ return {
173
+ revision: snapshotPasswordRevision(found.revision),
174
+ requirement
175
+ };
176
+ })),
177
+ requirements: (evidence) => read((state) => Effect.gen(function* () {
178
+ const captured = yield* snapshotAuthenticationEvidence(evidence);
179
+ if (!current(state, captured.revision)) return yield* StaleAuthentication.make({});
180
+ return requirement;
181
+ })),
182
+ approve: () => Effect.fail(unavailable())
183
+ });
184
+ const sessions = {
185
+ establish: (input, prepare) => transaction((state, journal, time) => Effect.gen(function* () {
186
+ if (input.pending !== void 0) return yield* PendingAuthenticationInvalid.make({});
187
+ if (input.handoffSourceSessionId !== void 0) return yield* unavailable();
188
+ const evidence = yield* snapshotAuthenticationEvidence(input.evidence);
189
+ if (!current(state, evidence.revision) || evidence.proofs.length !== 1 || input.session.subjectId !== evidence.revision.subjectId || input.session.securityRevision !== evidence.revision.securityRevision) return yield* StaleAuthentication.make({});
190
+ const assessed = yield* assessAuthentication(evidence, requirement, DateTime.makeUnsafe(time)).pipe(Effect.catchTag("SessionConfigurationError", unavailable));
191
+ const expires = DateTime.toEpochMillis(input.session.expiresAt);
192
+ const absolute = DateTime.toEpochMillis(input.session.absoluteExpiresAt);
193
+ const deadline = Math.min(expires, DateTime.toEpochMillis(evidence.proofs[0].verifiedAt) + requirement.maximumAgeMillis);
194
+ if (!assessed.satisfied || time >= deadline || expires > absolute) return yield* StaleAuthentication.make({});
195
+ if (state.sessions.some((row) => row.digest === input.session.digest)) return yield* SessionConflict.make({});
196
+ const row = {
197
+ ...input.session,
198
+ sessionId: SessionId.make(nextId(state)),
199
+ issuedAt: DateTime.makeUnsafe(time),
200
+ assurance: assessed.assurance,
201
+ provenance: yield* snapshotSessionAuthenticationProvenance({ evidence })
202
+ };
203
+ const encoded = yield* Schema.encodeEffect(sessionCodec)(row).pipe(Effect.mapError(unavailable));
204
+ const saved = yield* Schema.decodeEffect(sessionCodec)(encoded).pipe(Effect.mapError(unavailable));
205
+ state.sessions = [...state.sessions, saved];
206
+ return {
207
+ receipt: prepare(row, journal),
208
+ deadline
209
+ };
210
+ })),
211
+ verify: ({ digest }) => read((state, time) => {
212
+ const row = state.sessions.find((session) => session.digest === digest);
213
+ return row !== void 0 && live(state, row, time) ? Effect.succeed(row) : Effect.fail(SessionInvalid.make({}));
214
+ }),
215
+ rotate: (input, prepare) => transaction((state, journal, time) => Effect.gen(function* () {
216
+ const row = state.sessions.find((session) => session.digest === input.record.digest);
217
+ const expires = DateTime.toEpochMillis(input.nextExpiresAt);
218
+ if (row === void 0 || !live(state, row, time) || input.nextCredentialVersion === row.credentialVersion || expires <= time || expires > DateTime.toEpochMillis(row.absoluteExpiresAt) || state.sessions.some((session) => session.digest === input.nextDigest)) return yield* SessionConflict.make({});
219
+ if ((yield* Schema.encodeEffect(sessionCodec)(input.record).pipe(Effect.mapError(unavailable))) !== (yield* Schema.encodeEffect(sessionCodec)(row).pipe(Effect.mapError(unavailable)))) return yield* SessionConflict.make({});
220
+ const next = {
221
+ ...row,
222
+ digest: input.nextDigest,
223
+ credentialVersion: input.nextCredentialVersion,
224
+ issuedAt: DateTime.makeUnsafe(time),
225
+ expiresAt: input.nextExpiresAt
226
+ };
227
+ const encoded = yield* Schema.encodeEffect(sessionCodec)(next).pipe(Effect.mapError(unavailable));
228
+ const saved = yield* Schema.decodeEffect(sessionCodec)(encoded).pipe(Effect.mapError(unavailable));
229
+ state.sessions = state.sessions.map((session) => session.digest === row.digest ? saved : session);
230
+ return {
231
+ receipt: prepare(next, journal),
232
+ deadline: Math.min(expires, DateTime.toEpochMillis(row.expiresAt), DateTime.toEpochMillis(row.absoluteExpiresAt))
233
+ };
234
+ })),
235
+ revokeDigest: (digest, prepare) => transaction((state, journal) => Effect.sync(() => {
236
+ const found = state.sessions.some((row) => row.digest === digest);
237
+ state.sessions = state.sessions.filter((row) => row.digest !== digest);
238
+ return { receipt: prepare(found, journal) };
239
+ })),
240
+ revoke: (input, prepare) => transaction((state, journal) => Effect.sync(() => {
241
+ state.sessions = state.sessions.filter((row) => row.subjectId !== input.subjectId || row.sessionId !== input.sessionId);
242
+ return { receipt: prepare(void 0, journal) };
243
+ })),
244
+ revokeAll: (input, prepare) => transaction((state, journal) => Effect.gen(function* () {
245
+ const found = subject(state, input.subjectId);
246
+ if (found?.credential.revision.securityRevision !== input.expectedSecurityRevision) return yield* StaleAuthentication.make({});
247
+ const credential = yield* snapshotPasswordCredential({
248
+ ...found.credential,
249
+ revision: {
250
+ ...found.credential.revision,
251
+ securityRevision: SecurityRevision.make(nextId(state))
252
+ }
253
+ }).pipe(Effect.mapError(unavailable));
254
+ state.subjects = state.subjects.map((item) => item === found ? {
255
+ ...item,
256
+ credential
257
+ } : item);
258
+ return { receipt: prepare(void 0, journal) };
259
+ }))
260
+ };
261
+ const unsupported = () => Effect.fail(PasswordUnavailable.make({}));
262
+ const passwords = PasswordPersistence.of({
263
+ findCredential: (input) => read((state) => Effect.gen(function* () {
264
+ if (input.moduleId !== feature.moduleId) return yield* PasswordUnavailable.make({});
265
+ const found = state.subjects.find(({ active, credential }) => active && credential.identifier.namespace === input.identifier.namespace && credential.identifier.value === input.identifier.value && (input.subjectId === void 0 || credential.revision.subjectId === input.subjectId));
266
+ return found === void 0 ? Option.none() : Option.some(yield* snapshotPasswordCredential(found.credential));
267
+ })).pipe(Effect.mapError(() => PasswordUnavailable.make({}))),
268
+ rehashIfCurrent: (input) => transaction((state, journal) => Effect.gen(function* () {
269
+ const captured = yield* snapshotPasswordCredential(input.credential);
270
+ const nextVerifier = yield* Schema.decodeEffect(EncodedPasswordHash)(Redacted.value(input.nextVerifier));
271
+ const found = subject(state, captured.revision.subjectId);
272
+ if (found !== void 0 && captured.moduleId === feature.moduleId && (yield* Schema.encodeEffect(credentialCodec)(found.credential)) === (yield* Schema.encodeEffect(credentialCodec)(captured))) {
273
+ const credential = yield* snapshotPasswordCredential({
274
+ ...found.credential,
275
+ verifier: Redacted.make(nextVerifier),
276
+ verifierVersion: SecurityRevision.make(nextId(state))
277
+ });
278
+ state.subjects = state.subjects.map((item) => item === found ? {
279
+ ...item,
280
+ credential
281
+ } : item);
282
+ }
283
+ return { receipt: journal.prepare(void 0) };
284
+ })).pipe(Effect.flatMap((receipt) => receipt.read), Effect.mapError(() => PasswordUnavailable.make({}))),
285
+ readForSubject: unsupported,
286
+ recoveryTarget: unsupported,
287
+ addIfAbsent: unsupported,
288
+ replaceIfCurrent: unsupported,
289
+ resetWithProof: unsupported
290
+ });
291
+ return Context.make(AuthenticationAuthority, authority).pipe(Context.add(PasswordPersistence, passwords), Context.add(auth.sessions.StatefulSessionPersistence, sessions), Context.add(auth.sessions.SessionRepository, { list: (input) => read((state, time) => Effect.gen(function* () {
292
+ const limit = yield* Schema.decodeEffect(Schema.Int.check(Schema.isBetween({
293
+ minimum: 1,
294
+ maximum: 1e3
295
+ })))(input.limit).pipe(Effect.mapError(unavailable));
296
+ const cursor = input.cursor === void 0 ? void 0 : yield* Schema.decodeEffect(SessionId)(input.cursor).pipe(Effect.mapError(unavailable));
297
+ const rows = state.sessions.filter((row) => row.subjectId === input.subjectId && live(state, row, time) && (cursor === void 0 || row.sessionId > cursor)).sort((left, right) => left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1 : 0);
298
+ const page = rows.slice(0, limit);
299
+ const metadata = yield* Effect.forEach(page, (row) => Schema.decodeEffect(Schema.toType(SessionMetadata))(row)).pipe(Effect.mapError(unavailable));
300
+ const nextCursor = rows.length > limit ? page.at(-1)?.sessionId : void 0;
301
+ return {
302
+ sessions: metadata,
303
+ ...nextCursor === void 0 ? {} : { nextCursor }
304
+ };
305
+ })) }));
306
+ })).pipe(Layer.provide(hooksLayer));
307
+ //#endregion
308
+ export { PasswordSubject, layer, services };
309
+
310
+ //# sourceMappingURL=Testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Testing.mjs","names":[],"sources":["../src/Testing.ts"],"sourcesContent":["/** Explicit, runtime-independent test support. Never imported by production entrypoints. */\nimport {\n coordinateCommit,\n hasCommitScope,\n LifecycleHooks,\n type CommitJournal,\n type PreparedCommit,\n} from \"@yielded/auth/Hooks\";\nimport {\n EncodedPasswordHash,\n PasswordCredentialSnapshot,\n PasswordHashing,\n PasswordPersistence,\n PasswordUnavailable,\n snapshotPasswordCredential,\n snapshotPasswordRequirement,\n snapshotPasswordRevision,\n} from \"@yielded/auth/Password\";\nimport { hooksLayer } from \"@yielded/auth/Persistence\";\nimport { Email, SubjectId, TokenDigest } from \"@yielded/auth/Schema\";\nimport {\n type AuthenticationRequirement,\n assessAuthentication,\n AuthenticationAuthority,\n type AuthenticationRevision,\n PendingAuthenticationInvalid,\n SecurityRevision,\n SessionAuthenticationProvenance,\n SessionConflict,\n SessionCredentialVersion,\n SessionId,\n SessionInvalid,\n SessionMetadata,\n SessionUnavailable,\n snapshotAuthenticationEvidence,\n snapshotSessionAuthenticationProvenance,\n StaleAuthentication,\n type StatefulSessionPersistence,\n} from \"@yielded/auth/Sessions\";\nimport * as KdfAdmission from \"@yielded/crypto/KdfAdmission\";\nimport * as Portable from \"@yielded/crypto/Portable\";\nimport { layerCryptoWeb } from \"@yielded/crypto/WebCrypto\";\nimport {\n Clock,\n Context,\n Crypto,\n DateTime,\n Effect,\n Layer,\n Option,\n Random,\n Redacted,\n Schema,\n Semaphore,\n} from \"effect\";\n\nimport {\n PersistenceConfigurationError,\n type ClaimsCodec,\n type Definition,\n} from \"./internal/configuration\";\n\n/** One existing subject and one byte-preserving password. Seeding does not verify\n * an email or exercise registration/screening. Passwords are hashed on acquisition\n * by the supplied PasswordHashing service; they are never stored as plaintext. */\nexport const PasswordSubject = Schema.Struct({\n subjectId: SubjectId,\n email: Email,\n password: Schema.Redacted(Schema.String),\n active: Schema.optionalKey(Schema.Boolean),\n});\n\nexport interface Options {\n readonly subjects: ReadonlyArray<typeof PasswordSubject.Encoded>;\n /** Application policy; no permissive test default. */\n readonly requirement: AuthenticationRequirement;\n}\n\nconst configurationError = (reason: string) => PersistenceConfigurationError.make({ reason });\nconst unavailable = () => SessionUnavailable.make({});\n\nexport interface ServicesOptions {\n /** Reproducible test entropy. Omit to use the host's secure randomness. */\n readonly seed?: string | number;\n}\n\n/** Portable password hashing and Effect Crypto for tests. Requires global WebCrypto\n * at acquisition; uses the default password cost and one shared KDF admission owner.\n * A seed restarts its private random sequence on each acquisition; it never changes\n * the caller's Random or Clock services. Seeded credentials are only suitable for tests.\n * Provide this after composing Auth with Testing.layer and application claims.\n */\nexport const services = (options: ServicesOptions = {}) => {\n const { seed } = options;\n\n return Layer.unwrap(\n Effect.gen(function* () {\n const subtle = globalThis.crypto?.subtle;\n\n if (subtle === undefined)\n return yield* configurationError(\"Testing.services requires global WebCrypto\");\n\n const entropy =\n seed === undefined\n ? layerCryptoWeb\n : Layer.effect(\n Crypto.Crypto,\n Effect.gen(function* () {\n const native = yield* Crypto.Crypto;\n const random = yield* Random.Random.pipe(Random.withSeed(seed));\n\n return Crypto.make({\n randomBytes: (size) =>\n Uint8Array.from({ length: size }, () => random.nextIntUnsafe() & 255),\n digest: (algorithm, data) => native.digest(algorithm, data),\n });\n }),\n ).pipe(Layer.provide(layerCryptoWeb));\n\n const backend = Portable.layer(subtle).pipe(Layer.provideMerge(KdfAdmission.layer()));\n\n return PasswordHashing.layer().pipe(Layer.provide(backend), Layer.provideMerge(entropy));\n }),\n );\n};\n\n/** Fresh scoped memory for exactly one Password.make() sign-in strategy and\n * stateful sessions: issue, verify, renew, list, revoke and sign out. No SQL,\n * filesystem, native module, platform detection or implicit cryptography.\n * Uses the Effect Clock captured at acquisition, including TestClock.\n * Claims, hashing, Crypto, session policy and private delivery remain application\n * responsibilities. Other strategies, password management and session modes fail\n * at acquisition; pending authentication, handoff and ambient commits are unsupported.\n * Separate acquisitions own separate state, cleared on scope close. Reusing this\n * Layer within one build shares state. Never use for production authority or to\n * verify a production adapter's concurrency, durability or crash recovery.\n */\nexport const layer = <C extends ClaimsCodec, const Id extends string>(\n auth: Definition<C, Id>,\n options: Options,\n): Layer.Layer<\n | AuthenticationAuthority\n | PasswordPersistence\n | Context.Service.Identifier<Definition<C, Id>[\"sessions\"][\"StatefulSessionPersistence\"]>\n | Context.Service.Identifier<Definition<C, Id>[\"sessions\"][\"SessionRepository\"]>,\n PersistenceConfigurationError,\n PasswordHashing\n> =>\n Layer.effectContext(\n Effect.gen(function* () {\n const strategies = Object.values(auth.strategies);\n const feature = strategies[0]?.persistence;\n\n if (\n auth.sessionMode !== \"stateful\" ||\n strategies.length !== 1 ||\n feature?.kind !== \"password\" ||\n feature.management ||\n feature.lifecycle\n ) {\n return yield* configurationError(\n \"Testing.layer supports one sign-in-only Password.make() strategy and stateful sessions\",\n );\n }\n\n const seeds = yield* Schema.decodeEffect(Schema.Array(PasswordSubject))(\n options.subjects,\n ).pipe(Effect.mapError(() => configurationError(\"Invalid in-memory password subjects\")));\n\n const requirement = yield* snapshotPasswordRequirement(options.requirement).pipe(\n Effect.mapError(() => configurationError(\"Invalid in-memory authentication requirement\")),\n );\n\n const hasher = yield* PasswordHashing;\n const clock = yield* Clock.Clock;\n const hooks = yield* LifecycleHooks;\n const mutex = yield* Semaphore.make(1);\n const claims: Schema.Codec<C[\"Type\"], C[\"Encoded\"]> = auth.claims;\n\n const Session = Schema.Struct({\n ...SessionMetadata.fields,\n claims,\n digest: TokenDigest,\n provenance: SessionAuthenticationProvenance,\n credentialVersion: SessionCredentialVersion,\n });\n\n const Database = Schema.Struct({\n sequence: Schema.Natural,\n subjects: Schema.Array(\n Schema.Struct({ active: Schema.Boolean, credential: PasswordCredentialSnapshot }),\n ),\n sessions: Schema.Array(Session),\n });\n\n type State = { -readonly [K in keyof typeof Database.Type]: (typeof Database.Type)[K] };\n const codec = Schema.fromJsonString(Database);\n const sessionCodec = Schema.fromJsonString(Session);\n const credentialCodec = Schema.fromJsonString(PasswordCredentialSnapshot);\n const initial: State = { sequence: 0, subjects: [], sessions: [] };\n const ids = new Set<string>();\n const emails = new Set<string>();\n\n for (const seed of seeds) {\n if (ids.has(seed.subjectId) || emails.has(seed.email))\n return yield* configurationError(\"Duplicate in-memory subject ID or email\");\n ids.add(seed.subjectId);\n emails.add(seed.email);\n\n const verifier = yield* hasher\n .hash(seed.password)\n .pipe(Effect.mapError(() => configurationError(\"Cannot hash in-memory password seed\")));\n\n const credentialId = `password:${seed.subjectId}`;\n\n const credential = yield* Schema.decodeEffect(PasswordCredentialSnapshot)({\n moduleId: feature.moduleId,\n revision: {\n subjectId: seed.subjectId,\n securityRevision: \"1\",\n credentials: [{ credentialId, revision: \"1\" }],\n },\n credentialId,\n credentialRevision: \"1\",\n verifierVersion: \"1\",\n verifier: Redacted.value(verifier),\n normalization: \"none\",\n identifier: { namespace: \"email\", value: seed.email },\n identifierBindingRevision: \"1\",\n }).pipe(Effect.mapError(() => configurationError(\"Invalid in-memory password seed\")));\n\n initial.subjects = [...initial.subjects, { active: seed.active ?? true, credential }];\n }\n\n // Encoded snapshots detach private state from input, output and prepare callbacks.\n let stored: string | undefined = yield* Schema.encodeEffect(codec)(initial).pipe(\n Effect.mapError(() => configurationError(\"Cannot initialize in-memory storage\")),\n );\n\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n stored = undefined;\n }),\n );\n\n const load = Effect.suspend(() =>\n stored === undefined\n ? Effect.fail(unavailable())\n : Schema.decodeEffect(codec)(stored).pipe(Effect.mapError(unavailable)),\n );\n\n const now = () => clock.currentTimeMillisUnsafe();\n const nextId = (state: State) => `memory-${++state.sequence}`;\n\n const subject = (state: State, id: SubjectId) =>\n state.subjects.find((item) => item.active && item.credential.revision.subjectId === id);\n\n const current = (state: State, revision: AuthenticationRevision) => {\n const found = subject(state, revision.subjectId)?.credential;\n\n return (\n found !== undefined &&\n found.revision.securityRevision === revision.securityRevision &&\n revision.credentials.length === 1 &&\n revision.credentials[0]?.credentialId === found.credentialId &&\n revision.credentials[0]?.revision === found.credentialRevision\n );\n };\n\n const live = (state: State, session: typeof Session.Type, time: number) =>\n subject(state, session.subjectId)?.credential.revision.securityRevision ===\n session.securityRevision &&\n time >= DateTime.toEpochMillis(session.issuedAt) &&\n time <\n Math.min(\n DateTime.toEpochMillis(session.expiresAt),\n DateTime.toEpochMillis(session.absoluteExpiresAt),\n );\n\n const standalone = Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* unavailable();\n });\n\n const read = <A, E>(body: (state: State, time: number) => Effect.Effect<A, E>) =>\n standalone.pipe(\n Effect.andThen(mutex.withPermits(1)(Effect.flatMap(load, (state) => body(state, now())))),\n );\n\n // One local owner: failed/interrupted preparation never publishes its draft.\n // Hooks dispatch outside the mutex, after publishing; failed delivery never rolls back.\n const transaction = <A, E>(\n body: (\n state: State,\n journal: CommitJournal,\n time: number,\n ) => Effect.Effect<{ readonly receipt: PreparedCommit<A>; readonly deadline?: number }, E>,\n ) =>\n standalone.pipe(\n Effect.andThen(\n coordinateCommit((journal) =>\n mutex.withPermits(1)(\n Effect.uninterruptibleMask((restore) =>\n Effect.gen(function* () {\n const { receipt, deadline, time, encoded } = yield* restore(\n Effect.gen(function* () {\n const state = yield* load;\n const time = now();\n const { receipt, deadline = Infinity } = yield* body(state, journal, time);\n\n const encoded = yield* Schema.encodeEffect(codec)(state).pipe(\n Effect.mapError(unavailable),\n );\n\n const status = yield* Effect.result(receipt.read);\n\n if (status._tag !== \"Failure\" || status.failure._tag !== \"CommitPending\")\n return yield* unavailable();\n\n return { receipt, deadline, time, encoded };\n }),\n );\n\n // Publication and the owner's return form one uninterruptible commit.\n yield* Effect.try({\n try: () => {\n const finalTime = now();\n\n if (stored === undefined || finalTime < time || finalTime >= deadline)\n throw unavailable();\n stored = encoded;\n },\n catch: unavailable,\n });\n\n return receipt;\n }),\n ),\n ),\n ),\n ),\n Effect.map((result) => result.value),\n Effect.catchTag(\"HookConfigurationError\", unavailable),\n Effect.provideService(LifecycleHooks, hooks),\n );\n\n const authority = AuthenticationAuthority.of({\n capture: (id, anchors) =>\n read((state) =>\n Effect.gen(function* () {\n const found = subject(state, id)?.credential;\n\n if (\n found === undefined ||\n new Set(anchors).size !== anchors.length ||\n anchors.some((anchor) => anchor !== found.credentialId)\n )\n return yield* StaleAuthentication.make({});\n\n return { revision: snapshotPasswordRevision(found.revision), requirement };\n }),\n ),\n requirements: (evidence) =>\n read((state) =>\n Effect.gen(function* () {\n const captured = yield* snapshotAuthenticationEvidence(evidence);\n\n if (!current(state, captured.revision)) return yield* StaleAuthentication.make({});\n\n return requirement;\n }),\n ),\n // Signed sessions are deliberately outside this helper's coverage.\n approve: () => Effect.fail(unavailable()),\n });\n\n const sessions: StatefulSessionPersistence<C[\"Type\"]> = {\n establish: (input, prepare) =>\n transaction((state, journal, time) =>\n Effect.gen(function* () {\n if (input.pending !== undefined) return yield* PendingAuthenticationInvalid.make({});\n if (input.handoffSourceSessionId !== undefined) return yield* unavailable();\n const evidence = yield* snapshotAuthenticationEvidence(input.evidence);\n\n if (\n !current(state, evidence.revision) ||\n evidence.proofs.length !== 1 ||\n input.session.subjectId !== evidence.revision.subjectId ||\n input.session.securityRevision !== evidence.revision.securityRevision\n )\n return yield* StaleAuthentication.make({});\n\n const assessed = yield* assessAuthentication(\n evidence,\n requirement,\n DateTime.makeUnsafe(time),\n ).pipe(Effect.catchTag(\"SessionConfigurationError\", unavailable));\n\n const expires = DateTime.toEpochMillis(input.session.expiresAt);\n const absolute = DateTime.toEpochMillis(input.session.absoluteExpiresAt);\n\n const deadline = Math.min(\n expires,\n DateTime.toEpochMillis(evidence.proofs[0].verifiedAt) +\n requirement.maximumAgeMillis,\n );\n\n if (!assessed.satisfied || time >= deadline || expires > absolute)\n return yield* StaleAuthentication.make({});\n if (state.sessions.some((row) => row.digest === input.session.digest))\n return yield* SessionConflict.make({});\n\n const row = {\n ...input.session,\n sessionId: SessionId.make(nextId(state)),\n issuedAt: DateTime.makeUnsafe(time),\n assurance: assessed.assurance,\n provenance: yield* snapshotSessionAuthenticationProvenance({ evidence }),\n };\n\n const encoded = yield* Schema.encodeEffect(sessionCodec)(row).pipe(\n Effect.mapError(unavailable),\n );\n\n const saved = yield* Schema.decodeEffect(sessionCodec)(encoded).pipe(\n Effect.mapError(unavailable),\n );\n\n state.sessions = [...state.sessions, saved];\n\n return { receipt: prepare(row, journal), deadline };\n }),\n ),\n verify: ({ digest }) =>\n read((state, time) => {\n const row = state.sessions.find((session) => session.digest === digest);\n\n return row !== undefined && live(state, row, time)\n ? Effect.succeed(row)\n : Effect.fail(SessionInvalid.make({}));\n }),\n rotate: (input, prepare) =>\n transaction((state, journal, time) =>\n Effect.gen(function* () {\n const row = state.sessions.find((session) => session.digest === input.record.digest);\n const expires = DateTime.toEpochMillis(input.nextExpiresAt);\n\n if (\n row === undefined ||\n !live(state, row, time) ||\n input.nextCredentialVersion === row.credentialVersion ||\n expires <= time ||\n expires > DateTime.toEpochMillis(row.absoluteExpiresAt) ||\n state.sessions.some((session) => session.digest === input.nextDigest)\n )\n return yield* SessionConflict.make({});\n\n const expected = yield* Schema.encodeEffect(sessionCodec)(input.record).pipe(\n Effect.mapError(unavailable),\n );\n\n const actual = yield* Schema.encodeEffect(sessionCodec)(row).pipe(\n Effect.mapError(unavailable),\n );\n\n if (expected !== actual) return yield* SessionConflict.make({});\n\n const next = {\n ...row,\n digest: input.nextDigest,\n credentialVersion: input.nextCredentialVersion,\n issuedAt: DateTime.makeUnsafe(time),\n expiresAt: input.nextExpiresAt,\n };\n\n const encoded = yield* Schema.encodeEffect(sessionCodec)(next).pipe(\n Effect.mapError(unavailable),\n );\n\n const saved = yield* Schema.decodeEffect(sessionCodec)(encoded).pipe(\n Effect.mapError(unavailable),\n );\n\n state.sessions = state.sessions.map((session) =>\n session.digest === row.digest ? saved : session,\n );\n\n return {\n receipt: prepare(next, journal),\n deadline: Math.min(\n expires,\n DateTime.toEpochMillis(row.expiresAt),\n DateTime.toEpochMillis(row.absoluteExpiresAt),\n ),\n };\n }),\n ),\n revokeDigest: (digest, prepare) =>\n transaction((state, journal) =>\n Effect.sync(() => {\n const found = state.sessions.some((row) => row.digest === digest);\n\n state.sessions = state.sessions.filter((row) => row.digest !== digest);\n\n return { receipt: prepare(found, journal) };\n }),\n ),\n revoke: (input, prepare) =>\n transaction((state, journal) =>\n Effect.sync(() => {\n state.sessions = state.sessions.filter(\n (row) => row.subjectId !== input.subjectId || row.sessionId !== input.sessionId,\n );\n\n return { receipt: prepare(undefined, journal) };\n }),\n ),\n revokeAll: (input, prepare) =>\n transaction((state, journal) =>\n Effect.gen(function* () {\n const found = subject(state, input.subjectId);\n\n if (found?.credential.revision.securityRevision !== input.expectedSecurityRevision)\n return yield* StaleAuthentication.make({});\n\n const credential = yield* snapshotPasswordCredential({\n ...found.credential,\n revision: {\n ...found.credential.revision,\n securityRevision: SecurityRevision.make(nextId(state)),\n },\n }).pipe(Effect.mapError(unavailable));\n\n state.subjects = state.subjects.map((item) =>\n item === found ? { ...item, credential } : item,\n );\n\n return { receipt: prepare(undefined, journal) };\n }),\n ),\n };\n\n const unsupported = () => Effect.fail(PasswordUnavailable.make({}));\n\n const passwords = PasswordPersistence.of({\n findCredential: (input) =>\n read((state) =>\n Effect.gen(function* () {\n if (input.moduleId !== feature.moduleId) return yield* PasswordUnavailable.make({});\n\n const found = state.subjects.find(\n ({ active, credential }) =>\n active &&\n credential.identifier.namespace === input.identifier.namespace &&\n credential.identifier.value === input.identifier.value &&\n (input.subjectId === undefined ||\n credential.revision.subjectId === input.subjectId),\n );\n\n return found === undefined\n ? Option.none()\n : Option.some(yield* snapshotPasswordCredential(found.credential));\n }),\n ).pipe(Effect.mapError(() => PasswordUnavailable.make({}))),\n rehashIfCurrent: (input) =>\n transaction((state, journal) =>\n Effect.gen(function* () {\n const captured = yield* snapshotPasswordCredential(input.credential);\n\n const nextVerifier = yield* Schema.decodeEffect(EncodedPasswordHash)(\n Redacted.value(input.nextVerifier),\n );\n\n const found = subject(state, captured.revision.subjectId);\n\n if (\n found !== undefined &&\n captured.moduleId === feature.moduleId &&\n (yield* Schema.encodeEffect(credentialCodec)(found.credential)) ===\n (yield* Schema.encodeEffect(credentialCodec)(captured))\n ) {\n const credential = yield* snapshotPasswordCredential({\n ...found.credential,\n verifier: Redacted.make(nextVerifier),\n verifierVersion: SecurityRevision.make(nextId(state)),\n });\n\n state.subjects = state.subjects.map((item) =>\n item === found ? { ...item, credential } : item,\n );\n }\n\n return { receipt: journal.prepare(undefined) };\n }),\n ).pipe(\n Effect.flatMap((receipt) => receipt.read),\n Effect.mapError(() => PasswordUnavailable.make({})),\n ),\n readForSubject: unsupported,\n recoveryTarget: unsupported,\n addIfAbsent: unsupported,\n replaceIfCurrent: unsupported,\n resetWithProof: unsupported,\n });\n\n return Context.make(AuthenticationAuthority, authority).pipe(\n Context.add(PasswordPersistence, passwords),\n Context.add(auth.sessions.StatefulSessionPersistence, sessions),\n Context.add(auth.sessions.SessionRepository, {\n list: (input) =>\n read((state, time) =>\n Effect.gen(function* () {\n const limit = yield* Schema.decodeEffect(\n Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1000 })),\n )(input.limit).pipe(Effect.mapError(unavailable));\n\n const cursor =\n input.cursor === undefined\n ? undefined\n : yield* Schema.decodeEffect(SessionId)(input.cursor).pipe(\n Effect.mapError(unavailable),\n );\n\n const rows = state.sessions\n .filter(\n (row) =>\n row.subjectId === input.subjectId &&\n live(state, row, time) &&\n (cursor === undefined || row.sessionId > cursor),\n )\n .sort((left, right) =>\n left.sessionId < right.sessionId\n ? -1\n : left.sessionId > right.sessionId\n ? 1\n : 0,\n );\n\n const page = rows.slice(0, limit);\n\n const metadata = yield* Effect.forEach(page, (row) =>\n Schema.decodeEffect(Schema.toType(SessionMetadata))(row),\n ).pipe(Effect.mapError(unavailable));\n\n const nextCursor = rows.length > limit ? page.at(-1)?.sessionId : undefined;\n\n return { sessions: metadata, ...(nextCursor === undefined ? {} : { nextCursor }) };\n }),\n ),\n }),\n );\n }),\n ).pipe(Layer.provide(hooksLayer));\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { PreparedCommit } from "@yielded/auth/Hooks";
6
6
  import { CleanupResult } from "@yielded/auth/Persistence";
7
7
  import { AuthenticationRequirement, SecurityRevision } from "@yielded/auth/Sessions";
8
8
  import { TokenDigest } from "@yielded/auth/Schema";
9
- import { OAuthAccountRevision, OAuthActionAuthorization, OAuthCleanupInput, OAuthCommandId, OAuthExternalIdentity, OAuthRegistrationAccess, OAuthRegistrationDecision, OAuthRegistrationInspection, OAuthRegistrationIntent, OAuthUnavailable, PrepareOAuthCommit } from "@yielded/auth/OAuth";
9
+ import { OAuthAccountRevision, OAuthActionAuthorization, OAuthCleanupInput, OAuthCommandId, OAuthExternalIdentity, OAuthLinkedAccountsRead, OAuthRegistrationAccess, OAuthRegistrationDecision, OAuthRegistrationInspection, OAuthRegistrationIntent, OAuthUnavailable, PrepareOAuthCommit } from "@yielded/auth/OAuth";
10
10
  declare namespace oauth_model_d_exports {
11
11
  export { OAuthAccountsMapping, OAuthAction, OAuthAuthorityReadTable, OAuthAuthorityTable, OAuthCleanupDescriptor, OAuthCleanupTable, OAuthClock, OAuthCredentialReadTable, OAuthCredentialTable, OAuthD1Mapping, OAuthEligibilityDescriptor, OAuthEligibilityFact, OAuthEligibilityTable, OAuthFlowTable, OAuthOwnershipReadTable, OAuthOwnershipTable, OAuthRegistrationAuthority, OAuthRegistrationGuardDescriptor, OAuthRegistrationGuardTable, OAuthRegistrationIntentMapping, OAuthRegistrationIntentTable, OAuthRegistrationMapping, OAuthSignInMapping, OAuthSubjectReadTable, OAuthSubjectTable, oauthCleanupTable, oauthEligibilityTable, oauthRegistrationGuardTable, requiredOAuthRegistrationConstraints, requiredOAuthSignInConstraints };
12
12
  }
@@ -282,6 +282,15 @@ export interface OAuthAccountsMapping<S extends TableModel, O extends TableModel
282
282
  readonly subjectId: SubjectIdCodec<N>;
283
283
  readonly clock: OAuthClock<Expression>;
284
284
  readonly constraints: typeof requiredOAuthSignInConstraints;
285
+ /** Self-contained SQL predicate rechecking this invocation's current permission
286
+ * to view login metadata. Evaluated in every releasing read, alongside active
287
+ * subject/credential authority and ownership. Do not trust public assurance as
288
+ * credential provenance; bind session policy to application-owned rows as needed. */
289
+ readonly metadataAccess: (input: {
290
+ readonly invocation: OAuthLinkedAccountsRead["invocation"];
291
+ readonly moduleId: string;
292
+ readonly subjectId: N;
293
+ }) => Expression;
285
294
  readonly eligibility: ReadonlyArray<OAuthEligibilityDescriptor<N, Expression, Descriptor>>;
286
295
  readonly cleanup: ReadonlyArray<OAuthCleanupDescriptor<N, Expression, Descriptor>>;
287
296
  readonly sessionInvalidation: "same-authority-immediate" | "original-absolute-expiry";
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-model.mjs","names":[],"sources":["../../../src/internal/models/oauth-model.ts"],"sourcesContent":["import type { PreparedCommit } from \"@yielded/auth/Hooks\";\nimport type {\n OAuthActionAuthorization,\n OAuthAccountRevision,\n OAuthRegistrationIntent,\n OAuthExternalIdentity,\n OAuthRegistrationAccess,\n OAuthRegistrationInspection,\n OAuthUnavailable,\n OAuthCommandId,\n PrepareOAuthCommit,\n OAuthRegistrationDecision,\n OAuthCleanupInput,\n} from \"@yielded/auth/OAuth\";\nimport { AuthenticationFactor } from \"@yielded/auth/Operations\";\nimport type { CleanupResult } from \"@yielded/auth/Persistence\";\nimport type { TokenDigest } from \"@yielded/auth/Schema\";\nimport { type AuthenticationRequirement, SecurityRevision } from \"@yielded/auth/Sessions\";\nimport { type Effect, Schema } from \"effect\";\n\nimport type { TableModel as Table, SqlExpression } from \"../table-model\";\nimport type { PersistenceMappingError, SubjectIdCodec } from \"./common\";\n\ntype Column<T extends Table> = T[\"column\"];\nexport type OAuthAction = OAuthActionAuthorization[\"challenge\"][\"action\"];\n\nexport interface OAuthClock<Expression extends SqlExpression = SqlExpression> {\n readonly encodeInstant: (millis: number) => unknown;\n readonly decodeInstant: (value: unknown) => number;\n /** Actual engine wall clock sampled after locks, not transaction-start time.\n * PostgreSQL uses clock_timestamp(), never now()/CURRENT_TIMESTAMP. D1 repeats\n * its primary-engine sample bounds in final guards. Return integer milliseconds. */\n readonly engineNowMillis: Expression;\n /** Convert a stored timestamp expression to integer milliseconds. */\n readonly toMillis: (expression: Expression) => Expression;\n readonly fromMillis: (expression: Expression) => Expression;\n}\n\nexport interface OAuthSubjectReadTable<\n S extends Table,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: S[\"table\"];\n readonly id: Column<S>;\n readonly status: Column<S>;\n readonly securityRevision: Column<S>;\n readonly isActiveStatus: (value: unknown) => boolean;\n readonly activeCondition: Expression;\n readonly decodeAuthenticationRequirement: (row: S[\"select\"]) => AuthenticationRequirement;\n}\n\nexport interface OAuthSubjectTable<\n S extends Table,\n Expression extends SqlExpression = SqlExpression,\n> extends OAuthSubjectReadTable<S, Expression> {\n readonly nextSecurityRevision: (current: SecurityRevision) => SecurityRevision;\n}\n\nexport interface OAuthOwnershipReadTable<O extends Table, N> {\n readonly table: O[\"table\"];\n readonly identityKey: Column<O>;\n readonly provider: Column<O>;\n readonly issuer: Column<O>;\n readonly externalSubject: Column<O>;\n readonly subjectId: Column<O>;\n readonly decodeSubjectId: (row: O[\"select\"]) => N;\n}\n\nexport interface OAuthOwnershipTable<O extends Table, N> extends OAuthOwnershipReadTable<O, N> {\n readonly encodeInsert: (input: {\n readonly identity: typeof OAuthExternalIdentity.Type;\n readonly identityKey: string;\n readonly subjectId: N;\n }) => O[\"insert\"];\n}\n\nexport interface OAuthCredentialReadTable<\n C extends Table,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: C[\"table\"];\n readonly moduleId: Column<C>;\n readonly credentialId: Column<C>;\n readonly subjectId: Column<C>;\n readonly identityKey: Column<C>;\n readonly credentialRevision: Column<C>;\n readonly status: Column<C>;\n readonly isActiveStatus: (value: unknown) => boolean;\n readonly activeCondition: Expression;\n}\n\nexport interface OAuthCredentialTable<\n C extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> extends OAuthCredentialReadTable<C, Expression> {\n readonly encodeInsert: (input: {\n readonly moduleId: string;\n readonly subjectId: N;\n readonly identityKey: string;\n readonly credentialId: string;\n readonly credentialRevision: SecurityRevision;\n }) => C[\"insert\"];\n /** This slice removes login rows. Historical records belong in separate audit storage. */\n readonly removal: \"delete\";\n}\n\nexport interface OAuthAuthorityReadTable<\n C extends Table,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: C[\"table\"];\n readonly subjectId: Column<C>;\n readonly credentialId: Column<C>;\n readonly revision: Column<C>;\n readonly status: Column<C>;\n readonly isActiveStatus: (value: unknown) => boolean;\n readonly activeCondition: Expression;\n}\n\nexport interface OAuthAuthorityTable<\n C extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> extends OAuthAuthorityReadTable<C, Expression> {\n readonly encodeInsert: (input: {\n readonly subjectId: N;\n readonly credentialId: string;\n readonly revision: SecurityRevision;\n }) => C[\"insert\"];\n}\n\n/** Live ceremonies only. Predicate columns enforce access before deletion; the\n * snapshot retains the exact authenticated context and encrypted protocol data. */\nexport interface OAuthFlowTable<F extends Table> {\n readonly table: F[\"table\"];\n readonly moduleId: Column<F>;\n readonly flowId: Column<F>;\n readonly purpose: Column<F>;\n readonly generation: Column<F>;\n readonly provider: Column<F>;\n readonly callbackId: Column<F>;\n readonly issuer: Column<F>;\n readonly responseIssuerMode: Column<F>;\n readonly subjectId: Column<F>;\n readonly stateDigest: Column<F>;\n readonly binderVerifier: Column<F>;\n readonly binderExpiresAt: Column<F>;\n readonly snapshot: Column<F>;\n readonly issuedAt: Column<F>;\n readonly expiresAt: Column<F>;\n readonly encodeInsert: (input: {\n readonly moduleId: string;\n readonly flowId: string;\n readonly purpose: \"sign-in\" | \"link\" | \"connect\";\n }) => F[\"insert\"];\n}\n\nexport const OAuthEligibilityFact = Schema.Struct({\n credentialId: Schema.NonEmptyString.check(Schema.isMaxLength(256)),\n revision: SecurityRevision,\n usablePrimary: Schema.Boolean,\n factors: Schema.Array(AuthenticationFactor).check(Schema.isMaxLength(8)),\n userVerified: Schema.Boolean,\n phishingResistant: Schema.Boolean,\n});\n\nexport type OAuthEligibilityFact = typeof OAuthEligibilityFact.Type;\n\n/** Each descriptor reads only its own subject-scoped method rows. The native row\n * projector and SQL condition must express the same installed method eligibility.\n * All source/identifier/policy mutations advance subject securityRevision. D1 also\n * guards exact selected IDs/versions and absence/count assumptions, never just a\n * stale boolean. An over-limit graph fails closed rather than truncating. */\nexport interface OAuthEligibilityTable<\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: T[\"table\"];\n readonly subjectId: Column<T>;\n readonly credentialId: Column<T>;\n readonly revision: Column<T>;\n readonly scope: string;\n readonly condition: (subjectId: N) => Expression;\n readonly decode: (row: T[\"select\"]) => OAuthEligibilityFact | undefined;\n}\n\nexport interface OAuthCleanupTable<\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: T[\"table\"];\n readonly subjectId: Column<T>;\n /** Bound to this subject's old revision; no opaque independently committing service. */\n readonly condition: (input: {\n readonly subjectId: N;\n readonly revision: OAuthAccountRevision;\n readonly removedCredentialId?: string;\n }) => Expression;\n readonly disposition: \"delete\";\n}\n\nexport interface OAuthRegistrationGuardTable<\n T extends Table,\n Registration,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: T[\"table\"];\n readonly orderBy: Column<T>;\n readonly condition: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly nativeSubjectId?: N;\n }) => Expression;\n}\n\nexport interface OAuthRegistrationGuardDescriptor<\n Registration,\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly table: Descriptor;\n readonly orderBy: string;\n readonly condition: OAuthRegistrationGuardTable<Table, Registration, N, Expression>[\"condition\"];\n}\n\nexport const oauthRegistrationGuardTable = <\n T extends Table,\n Registration,\n N,\n Expression extends SqlExpression = SqlExpression,\n>(\n input: OAuthRegistrationGuardTable<T, Registration, N, Expression>,\n): OAuthRegistrationGuardDescriptor<Registration, N, Expression, T[\"table\"]> =>\n Object.freeze({\n table: input.table,\n orderBy: input.orderBy,\n condition: input.condition,\n });\n\n/** Heterogeneous descriptors are made by typed factories and projected privately. */\nexport interface OAuthEligibilityDescriptor<\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly _tag: \"OAuthEligibilityDescriptor\";\n readonly scope: string;\n readonly table: Descriptor;\n readonly subjectId: string;\n readonly credentialId: string;\n readonly revision: string;\n readonly condition: (subjectId: N) => Expression;\n readonly decode: (row: unknown) => OAuthEligibilityFact | undefined;\n}\n\nexport const oauthEligibilityTable = <\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n>(\n input: OAuthEligibilityTable<T, N, Expression>,\n): OAuthEligibilityDescriptor<N, Expression, T[\"table\"]> => {\n const { table, subjectId, credentialId, revision, scope, condition, decode } = input;\n\n return Object.freeze({\n _tag: \"OAuthEligibilityDescriptor\",\n table,\n subjectId,\n credentialId,\n revision,\n scope,\n condition,\n // The query owner supplies rows selected from this descriptor's mapped table.\n decode: (row: unknown) => decode(row as T[\"select\"]),\n });\n};\n\nexport interface OAuthCleanupDescriptor<\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly table: Descriptor;\n readonly subjectId: string;\n readonly condition: OAuthCleanupTable<Table, N, Expression>[\"condition\"];\n}\n\nexport const oauthCleanupTable = <\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n>(\n input: OAuthCleanupTable<T, N, Expression>,\n): OAuthCleanupDescriptor<N, Expression, T[\"table\"]> =>\n Object.freeze({ table: input.table, subjectId: input.subjectId, condition: input.condition });\n\n/** Original application binding and retained decision are encoded in snapshot. */\nexport interface OAuthRegistrationIntentTable<I extends Table> {\n readonly table: I[\"table\"];\n readonly moduleId: Column<I>;\n readonly reference: Column<I>;\n readonly flowId: Column<I>;\n readonly identityKey: Column<I>;\n readonly snapshot: Column<I>;\n readonly expiresAt: Column<I>;\n readonly retentionUntil: Column<I>;\n readonly encodeInsert: (intent: OAuthRegistrationIntent) => I[\"insert\"];\n}\n\nexport interface OAuthD1Mapping {\n readonly d1: { readonly primary: true };\n}\n\nexport const requiredOAuthSignInConstraints = {\n flow: \"unique(flow.moduleId,flow.flowId)\",\n stateDigest: \"unique(flow.stateDigest)\",\n ownership: \"unique(ownership.identityKey)\",\n credentialId: \"unique(credential.credentialId)\",\n credentialIdentity: \"unique(credential.identityKey)\",\n authorityCredential: \"unique(authority.subjectId,authority.credentialId)\",\n} as const;\n\nexport const requiredOAuthRegistrationConstraints = {\n intentReference: \"unique(intent.moduleId,intent.reference)\",\n intentFlow: \"unique(intent.moduleId,intent.flowId)\",\n ownership: \"unique(ownership.identityKey)\",\n credentialId: \"unique(credential.credentialId)\",\n credentialIdentity: \"unique(credential.identityKey)\",\n authorityCredential: \"unique(authority.subjectId,authority.credentialId)\",\n} as const;\n\nexport interface OAuthSignInMapping<\n S extends Table,\n O extends Table,\n C extends Table,\n AC extends Table,\n F extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly subject: OAuthSubjectReadTable<S, Expression>;\n readonly ownership: OAuthOwnershipReadTable<O, N>;\n readonly credential: OAuthCredentialReadTable<C, Expression>;\n readonly authority: OAuthAuthorityReadTable<AC, Expression>;\n readonly flow: OAuthFlowTable<F>;\n readonly subjectId: SubjectIdCodec<N>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: typeof requiredOAuthSignInConstraints;\n}\n\nexport interface OAuthRegistrationIntentMapping<\n O extends Table,\n I extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly ownership: OAuthOwnershipReadTable<O, N>;\n readonly intent: OAuthRegistrationIntentTable<I>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: Pick<\n typeof requiredOAuthRegistrationConstraints,\n \"intentReference\" | \"intentFlow\" | \"ownership\"\n >;\n readonly eligible: (intent: OAuthRegistrationIntent) => Expression;\n}\n\nexport interface OAuthRegistrationMapping<\n Registration,\n S extends Table,\n O extends Table,\n C extends Table,\n AC extends Table,\n I extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly registration: Schema.Codec<Registration, unknown, never, never>;\n readonly ownership: OAuthOwnershipTable<O, N>;\n readonly intent: OAuthRegistrationIntentTable<I>;\n readonly subject: OAuthSubjectReadTable<S, Expression>;\n readonly credential: OAuthCredentialTable<C, N, Expression>;\n readonly authority: OAuthAuthorityTable<AC, N, Expression>;\n readonly subjectId: SubjectIdCodec<N>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: typeof requiredOAuthRegistrationConstraints;\n readonly inspect: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n }) => Effect.Effect<\n { readonly fingerprint: string; readonly eligible: boolean },\n PersistenceMappingError\n >;\n readonly eligibility: {\n readonly guards?: ReadonlyArray<\n OAuthRegistrationGuardDescriptor<Registration, N, Expression, Descriptor>\n >;\n readonly admission: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly nativeSubjectId?: N;\n }) => Expression;\n readonly postcondition: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly nativeSubjectId?: N;\n }) => Expression;\n };\n readonly allocateSubjectId: Effect.Effect<N, PersistenceMappingError>;\n readonly allocateCredentialId: Effect.Effect<string, PersistenceMappingError>;\n readonly allocateRevision: Effect.Effect<SecurityRevision, PersistenceMappingError>;\n readonly encodeSubjectInsert: (\n input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly requestId: string;\n },\n ids: { readonly subjectId: N; readonly securityRevision: SecurityRevision },\n ) => S[\"insert\"];\n}\n\nexport interface OAuthAccountsMapping<\n S extends Table,\n O extends Table,\n C extends Table,\n AC extends Table,\n F extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly subject: OAuthSubjectTable<S, Expression>;\n readonly ownership: OAuthOwnershipTable<O, N>;\n readonly credential: OAuthCredentialTable<C, N, Expression>;\n readonly authority: OAuthAuthorityTable<AC, N, Expression>;\n readonly flow: OAuthFlowTable<F>;\n readonly subjectId: SubjectIdCodec<N>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: typeof requiredOAuthSignInConstraints;\n readonly eligibility: ReadonlyArray<OAuthEligibilityDescriptor<N, Expression, Descriptor>>;\n readonly cleanup: ReadonlyArray<OAuthCleanupDescriptor<N, Expression, Descriptor>>;\n readonly sessionInvalidation: \"same-authority-immediate\" | \"original-absolute-expiry\";\n /** Indexed same-owner reference predicate for any installed grant/application references. */\n readonly otherReferences: (input: {\n readonly identityKey: string;\n readonly subjectId: N;\n }) => Expression;\n readonly allocateCredentialId: Effect.Effect<string, PersistenceMappingError>;\n readonly allocateRevision: Effect.Effect<SecurityRevision, PersistenceMappingError>;\n}\n\nexport interface OAuthRegistrationAuthority<Registration> {\n readonly read: (\n access: OAuthRegistrationAccess,\n ) => Effect.Effect<OAuthRegistrationInspection | undefined, OAuthUnavailable>;\n readonly inspect: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n }) => Effect.Effect<\n {\n readonly fingerprint: TokenDigest;\n readonly eligible: boolean;\n },\n OAuthUnavailable\n >;\n readonly register: <A>(\n input: {\n readonly access: OAuthRegistrationAccess;\n readonly intent: OAuthRegistrationIntent;\n readonly commandId: typeof OAuthCommandId.Type;\n readonly registration: Registration;\n readonly fingerprint: TokenDigest;\n readonly payload: string;\n readonly requestId: string;\n },\n prepare: PrepareOAuthCommit<OAuthRegistrationDecision, A>,\n ) => Effect.Effect<PreparedCommit<A>, OAuthUnavailable>;\n readonly cleanup: <A>(\n input: OAuthCleanupInput,\n prepare: PrepareOAuthCommit<CleanupResult, A>,\n ) => Effect.Effect<PreparedCommit<A>, OAuthUnavailable>;\n}\n"],"mappings":";;;;AA8JA,MAAa,uBAAuB,OAAO,OAAO;CAChD,cAAc,OAAO,eAAe,MAAM,OAAO,YAAY,GAAG,CAAC;CACjE,UAAU;CACV,eAAe,OAAO;CACtB,SAAS,OAAO,MAAM,oBAAoB,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;CACvE,cAAc,OAAO;CACrB,mBAAmB,OAAO;AAC5B,CAAC;AAiED,MAAa,+BAMX,UAEA,OAAO,OAAO;CACZ,OAAO,MAAM;CACb,SAAS,MAAM;CACf,WAAW,MAAM;AACnB,CAAC;AAkBH,MAAa,yBAKX,UAC0D;CAC1D,MAAM,EAAE,OAAO,WAAW,cAAc,UAAU,OAAO,WAAW,WAAW;CAE/E,OAAO,OAAO,OAAO;EACnB,MAAM;EACN;EACA;EACA;EACA;EACA;EACA;EAEA,SAAS,QAAiB,OAAO,GAAkB;CACrD,CAAC;AACH;AAYA,MAAa,qBAKX,UAEA,OAAO,OAAO;CAAE,OAAO,MAAM;CAAO,WAAW,MAAM;CAAW,WAAW,MAAM;AAAU,CAAC;AAmB9F,MAAa,iCAAiC;CAC5C,MAAM;CACN,aAAa;CACb,WAAW;CACX,cAAc;CACd,oBAAoB;CACpB,qBAAqB;AACvB;AAEA,MAAa,uCAAuC;CAClD,iBAAiB;CACjB,YAAY;CACZ,WAAW;CACX,cAAc;CACd,oBAAoB;CACpB,qBAAqB;AACvB"}
1
+ {"version":3,"file":"oauth-model.mjs","names":[],"sources":["../../../src/internal/models/oauth-model.ts"],"sourcesContent":["import type { PreparedCommit } from \"@yielded/auth/Hooks\";\nimport type {\n OAuthActionAuthorization,\n OAuthAccountRevision,\n OAuthLinkedAccountsRead,\n OAuthRegistrationIntent,\n OAuthExternalIdentity,\n OAuthRegistrationAccess,\n OAuthRegistrationInspection,\n OAuthUnavailable,\n OAuthCommandId,\n PrepareOAuthCommit,\n OAuthRegistrationDecision,\n OAuthCleanupInput,\n} from \"@yielded/auth/OAuth\";\nimport { AuthenticationFactor } from \"@yielded/auth/Operations\";\nimport type { CleanupResult } from \"@yielded/auth/Persistence\";\nimport type { TokenDigest } from \"@yielded/auth/Schema\";\nimport { type AuthenticationRequirement, SecurityRevision } from \"@yielded/auth/Sessions\";\nimport { type Effect, Schema } from \"effect\";\n\nimport type { TableModel as Table, SqlExpression } from \"../table-model\";\nimport type { PersistenceMappingError, SubjectIdCodec } from \"./common\";\n\ntype Column<T extends Table> = T[\"column\"];\nexport type OAuthAction = OAuthActionAuthorization[\"challenge\"][\"action\"];\n\nexport interface OAuthClock<Expression extends SqlExpression = SqlExpression> {\n readonly encodeInstant: (millis: number) => unknown;\n readonly decodeInstant: (value: unknown) => number;\n /** Actual engine wall clock sampled after locks, not transaction-start time.\n * PostgreSQL uses clock_timestamp(), never now()/CURRENT_TIMESTAMP. D1 repeats\n * its primary-engine sample bounds in final guards. Return integer milliseconds. */\n readonly engineNowMillis: Expression;\n /** Convert a stored timestamp expression to integer milliseconds. */\n readonly toMillis: (expression: Expression) => Expression;\n readonly fromMillis: (expression: Expression) => Expression;\n}\n\nexport interface OAuthSubjectReadTable<\n S extends Table,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: S[\"table\"];\n readonly id: Column<S>;\n readonly status: Column<S>;\n readonly securityRevision: Column<S>;\n readonly isActiveStatus: (value: unknown) => boolean;\n readonly activeCondition: Expression;\n readonly decodeAuthenticationRequirement: (row: S[\"select\"]) => AuthenticationRequirement;\n}\n\nexport interface OAuthSubjectTable<\n S extends Table,\n Expression extends SqlExpression = SqlExpression,\n> extends OAuthSubjectReadTable<S, Expression> {\n readonly nextSecurityRevision: (current: SecurityRevision) => SecurityRevision;\n}\n\nexport interface OAuthOwnershipReadTable<O extends Table, N> {\n readonly table: O[\"table\"];\n readonly identityKey: Column<O>;\n readonly provider: Column<O>;\n readonly issuer: Column<O>;\n readonly externalSubject: Column<O>;\n readonly subjectId: Column<O>;\n readonly decodeSubjectId: (row: O[\"select\"]) => N;\n}\n\nexport interface OAuthOwnershipTable<O extends Table, N> extends OAuthOwnershipReadTable<O, N> {\n readonly encodeInsert: (input: {\n readonly identity: typeof OAuthExternalIdentity.Type;\n readonly identityKey: string;\n readonly subjectId: N;\n }) => O[\"insert\"];\n}\n\nexport interface OAuthCredentialReadTable<\n C extends Table,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: C[\"table\"];\n readonly moduleId: Column<C>;\n readonly credentialId: Column<C>;\n readonly subjectId: Column<C>;\n readonly identityKey: Column<C>;\n readonly credentialRevision: Column<C>;\n readonly status: Column<C>;\n readonly isActiveStatus: (value: unknown) => boolean;\n readonly activeCondition: Expression;\n}\n\nexport interface OAuthCredentialTable<\n C extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> extends OAuthCredentialReadTable<C, Expression> {\n readonly encodeInsert: (input: {\n readonly moduleId: string;\n readonly subjectId: N;\n readonly identityKey: string;\n readonly credentialId: string;\n readonly credentialRevision: SecurityRevision;\n }) => C[\"insert\"];\n /** This slice removes login rows. Historical records belong in separate audit storage. */\n readonly removal: \"delete\";\n}\n\nexport interface OAuthAuthorityReadTable<\n C extends Table,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: C[\"table\"];\n readonly subjectId: Column<C>;\n readonly credentialId: Column<C>;\n readonly revision: Column<C>;\n readonly status: Column<C>;\n readonly isActiveStatus: (value: unknown) => boolean;\n readonly activeCondition: Expression;\n}\n\nexport interface OAuthAuthorityTable<\n C extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> extends OAuthAuthorityReadTable<C, Expression> {\n readonly encodeInsert: (input: {\n readonly subjectId: N;\n readonly credentialId: string;\n readonly revision: SecurityRevision;\n }) => C[\"insert\"];\n}\n\n/** Live ceremonies only. Predicate columns enforce access before deletion; the\n * snapshot retains the exact authenticated context and encrypted protocol data. */\nexport interface OAuthFlowTable<F extends Table> {\n readonly table: F[\"table\"];\n readonly moduleId: Column<F>;\n readonly flowId: Column<F>;\n readonly purpose: Column<F>;\n readonly generation: Column<F>;\n readonly provider: Column<F>;\n readonly callbackId: Column<F>;\n readonly issuer: Column<F>;\n readonly responseIssuerMode: Column<F>;\n readonly subjectId: Column<F>;\n readonly stateDigest: Column<F>;\n readonly binderVerifier: Column<F>;\n readonly binderExpiresAt: Column<F>;\n readonly snapshot: Column<F>;\n readonly issuedAt: Column<F>;\n readonly expiresAt: Column<F>;\n readonly encodeInsert: (input: {\n readonly moduleId: string;\n readonly flowId: string;\n readonly purpose: \"sign-in\" | \"link\" | \"connect\";\n }) => F[\"insert\"];\n}\n\nexport const OAuthEligibilityFact = Schema.Struct({\n credentialId: Schema.NonEmptyString.check(Schema.isMaxLength(256)),\n revision: SecurityRevision,\n usablePrimary: Schema.Boolean,\n factors: Schema.Array(AuthenticationFactor).check(Schema.isMaxLength(8)),\n userVerified: Schema.Boolean,\n phishingResistant: Schema.Boolean,\n});\n\nexport type OAuthEligibilityFact = typeof OAuthEligibilityFact.Type;\n\n/** Each descriptor reads only its own subject-scoped method rows. The native row\n * projector and SQL condition must express the same installed method eligibility.\n * All source/identifier/policy mutations advance subject securityRevision. D1 also\n * guards exact selected IDs/versions and absence/count assumptions, never just a\n * stale boolean. An over-limit graph fails closed rather than truncating. */\nexport interface OAuthEligibilityTable<\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: T[\"table\"];\n readonly subjectId: Column<T>;\n readonly credentialId: Column<T>;\n readonly revision: Column<T>;\n readonly scope: string;\n readonly condition: (subjectId: N) => Expression;\n readonly decode: (row: T[\"select\"]) => OAuthEligibilityFact | undefined;\n}\n\nexport interface OAuthCleanupTable<\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: T[\"table\"];\n readonly subjectId: Column<T>;\n /** Bound to this subject's old revision; no opaque independently committing service. */\n readonly condition: (input: {\n readonly subjectId: N;\n readonly revision: OAuthAccountRevision;\n readonly removedCredentialId?: string;\n }) => Expression;\n readonly disposition: \"delete\";\n}\n\nexport interface OAuthRegistrationGuardTable<\n T extends Table,\n Registration,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly table: T[\"table\"];\n readonly orderBy: Column<T>;\n readonly condition: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly nativeSubjectId?: N;\n }) => Expression;\n}\n\nexport interface OAuthRegistrationGuardDescriptor<\n Registration,\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly table: Descriptor;\n readonly orderBy: string;\n readonly condition: OAuthRegistrationGuardTable<Table, Registration, N, Expression>[\"condition\"];\n}\n\nexport const oauthRegistrationGuardTable = <\n T extends Table,\n Registration,\n N,\n Expression extends SqlExpression = SqlExpression,\n>(\n input: OAuthRegistrationGuardTable<T, Registration, N, Expression>,\n): OAuthRegistrationGuardDescriptor<Registration, N, Expression, T[\"table\"]> =>\n Object.freeze({\n table: input.table,\n orderBy: input.orderBy,\n condition: input.condition,\n });\n\n/** Heterogeneous descriptors are made by typed factories and projected privately. */\nexport interface OAuthEligibilityDescriptor<\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly _tag: \"OAuthEligibilityDescriptor\";\n readonly scope: string;\n readonly table: Descriptor;\n readonly subjectId: string;\n readonly credentialId: string;\n readonly revision: string;\n readonly condition: (subjectId: N) => Expression;\n readonly decode: (row: unknown) => OAuthEligibilityFact | undefined;\n}\n\nexport const oauthEligibilityTable = <\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n>(\n input: OAuthEligibilityTable<T, N, Expression>,\n): OAuthEligibilityDescriptor<N, Expression, T[\"table\"]> => {\n const { table, subjectId, credentialId, revision, scope, condition, decode } = input;\n\n return Object.freeze({\n _tag: \"OAuthEligibilityDescriptor\",\n table,\n subjectId,\n credentialId,\n revision,\n scope,\n condition,\n // The query owner supplies rows selected from this descriptor's mapped table.\n decode: (row: unknown) => decode(row as T[\"select\"]),\n });\n};\n\nexport interface OAuthCleanupDescriptor<\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly table: Descriptor;\n readonly subjectId: string;\n readonly condition: OAuthCleanupTable<Table, N, Expression>[\"condition\"];\n}\n\nexport const oauthCleanupTable = <\n T extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n>(\n input: OAuthCleanupTable<T, N, Expression>,\n): OAuthCleanupDescriptor<N, Expression, T[\"table\"]> =>\n Object.freeze({ table: input.table, subjectId: input.subjectId, condition: input.condition });\n\n/** Original application binding and retained decision are encoded in snapshot. */\nexport interface OAuthRegistrationIntentTable<I extends Table> {\n readonly table: I[\"table\"];\n readonly moduleId: Column<I>;\n readonly reference: Column<I>;\n readonly flowId: Column<I>;\n readonly identityKey: Column<I>;\n readonly snapshot: Column<I>;\n readonly expiresAt: Column<I>;\n readonly retentionUntil: Column<I>;\n readonly encodeInsert: (intent: OAuthRegistrationIntent) => I[\"insert\"];\n}\n\nexport interface OAuthD1Mapping {\n readonly d1: { readonly primary: true };\n}\n\nexport const requiredOAuthSignInConstraints = {\n flow: \"unique(flow.moduleId,flow.flowId)\",\n stateDigest: \"unique(flow.stateDigest)\",\n ownership: \"unique(ownership.identityKey)\",\n credentialId: \"unique(credential.credentialId)\",\n credentialIdentity: \"unique(credential.identityKey)\",\n authorityCredential: \"unique(authority.subjectId,authority.credentialId)\",\n} as const;\n\nexport const requiredOAuthRegistrationConstraints = {\n intentReference: \"unique(intent.moduleId,intent.reference)\",\n intentFlow: \"unique(intent.moduleId,intent.flowId)\",\n ownership: \"unique(ownership.identityKey)\",\n credentialId: \"unique(credential.credentialId)\",\n credentialIdentity: \"unique(credential.identityKey)\",\n authorityCredential: \"unique(authority.subjectId,authority.credentialId)\",\n} as const;\n\nexport interface OAuthSignInMapping<\n S extends Table,\n O extends Table,\n C extends Table,\n AC extends Table,\n F extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly subject: OAuthSubjectReadTable<S, Expression>;\n readonly ownership: OAuthOwnershipReadTable<O, N>;\n readonly credential: OAuthCredentialReadTable<C, Expression>;\n readonly authority: OAuthAuthorityReadTable<AC, Expression>;\n readonly flow: OAuthFlowTable<F>;\n readonly subjectId: SubjectIdCodec<N>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: typeof requiredOAuthSignInConstraints;\n}\n\nexport interface OAuthRegistrationIntentMapping<\n O extends Table,\n I extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n> {\n readonly ownership: OAuthOwnershipReadTable<O, N>;\n readonly intent: OAuthRegistrationIntentTable<I>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: Pick<\n typeof requiredOAuthRegistrationConstraints,\n \"intentReference\" | \"intentFlow\" | \"ownership\"\n >;\n readonly eligible: (intent: OAuthRegistrationIntent) => Expression;\n}\n\nexport interface OAuthRegistrationMapping<\n Registration,\n S extends Table,\n O extends Table,\n C extends Table,\n AC extends Table,\n I extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly registration: Schema.Codec<Registration, unknown, never, never>;\n readonly ownership: OAuthOwnershipTable<O, N>;\n readonly intent: OAuthRegistrationIntentTable<I>;\n readonly subject: OAuthSubjectReadTable<S, Expression>;\n readonly credential: OAuthCredentialTable<C, N, Expression>;\n readonly authority: OAuthAuthorityTable<AC, N, Expression>;\n readonly subjectId: SubjectIdCodec<N>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: typeof requiredOAuthRegistrationConstraints;\n readonly inspect: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n }) => Effect.Effect<\n { readonly fingerprint: string; readonly eligible: boolean },\n PersistenceMappingError\n >;\n readonly eligibility: {\n readonly guards?: ReadonlyArray<\n OAuthRegistrationGuardDescriptor<Registration, N, Expression, Descriptor>\n >;\n readonly admission: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly nativeSubjectId?: N;\n }) => Expression;\n readonly postcondition: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly nativeSubjectId?: N;\n }) => Expression;\n };\n readonly allocateSubjectId: Effect.Effect<N, PersistenceMappingError>;\n readonly allocateCredentialId: Effect.Effect<string, PersistenceMappingError>;\n readonly allocateRevision: Effect.Effect<SecurityRevision, PersistenceMappingError>;\n readonly encodeSubjectInsert: (\n input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n readonly requestId: string;\n },\n ids: { readonly subjectId: N; readonly securityRevision: SecurityRevision },\n ) => S[\"insert\"];\n}\n\nexport interface OAuthAccountsMapping<\n S extends Table,\n O extends Table,\n C extends Table,\n AC extends Table,\n F extends Table,\n N,\n Expression extends SqlExpression = SqlExpression,\n Descriptor extends object = object,\n> {\n readonly subject: OAuthSubjectTable<S, Expression>;\n readonly ownership: OAuthOwnershipTable<O, N>;\n readonly credential: OAuthCredentialTable<C, N, Expression>;\n readonly authority: OAuthAuthorityTable<AC, N, Expression>;\n readonly flow: OAuthFlowTable<F>;\n readonly subjectId: SubjectIdCodec<N>;\n readonly clock: OAuthClock<Expression>;\n readonly constraints: typeof requiredOAuthSignInConstraints;\n /** Self-contained SQL predicate rechecking this invocation's current permission\n * to view login metadata. Evaluated in every releasing read, alongside active\n * subject/credential authority and ownership. Do not trust public assurance as\n * credential provenance; bind session policy to application-owned rows as needed. */\n readonly metadataAccess: (input: {\n readonly invocation: OAuthLinkedAccountsRead[\"invocation\"];\n readonly moduleId: string;\n readonly subjectId: N;\n }) => Expression;\n readonly eligibility: ReadonlyArray<OAuthEligibilityDescriptor<N, Expression, Descriptor>>;\n readonly cleanup: ReadonlyArray<OAuthCleanupDescriptor<N, Expression, Descriptor>>;\n readonly sessionInvalidation: \"same-authority-immediate\" | \"original-absolute-expiry\";\n /** Indexed same-owner reference predicate for any installed grant/application references. */\n readonly otherReferences: (input: {\n readonly identityKey: string;\n readonly subjectId: N;\n }) => Expression;\n readonly allocateCredentialId: Effect.Effect<string, PersistenceMappingError>;\n readonly allocateRevision: Effect.Effect<SecurityRevision, PersistenceMappingError>;\n}\n\nexport interface OAuthRegistrationAuthority<Registration> {\n readonly read: (\n access: OAuthRegistrationAccess,\n ) => Effect.Effect<OAuthRegistrationInspection | undefined, OAuthUnavailable>;\n readonly inspect: (input: {\n readonly intent: OAuthRegistrationIntent;\n readonly registration: Registration;\n }) => Effect.Effect<\n {\n readonly fingerprint: TokenDigest;\n readonly eligible: boolean;\n },\n OAuthUnavailable\n >;\n readonly register: <A>(\n input: {\n readonly access: OAuthRegistrationAccess;\n readonly intent: OAuthRegistrationIntent;\n readonly commandId: typeof OAuthCommandId.Type;\n readonly registration: Registration;\n readonly fingerprint: TokenDigest;\n readonly payload: string;\n readonly requestId: string;\n },\n prepare: PrepareOAuthCommit<OAuthRegistrationDecision, A>,\n ) => Effect.Effect<PreparedCommit<A>, OAuthUnavailable>;\n readonly cleanup: <A>(\n input: OAuthCleanupInput,\n prepare: PrepareOAuthCommit<CleanupResult, A>,\n ) => Effect.Effect<PreparedCommit<A>, OAuthUnavailable>;\n}\n"],"mappings":";;;;AA+JA,MAAa,uBAAuB,OAAO,OAAO;CAChD,cAAc,OAAO,eAAe,MAAM,OAAO,YAAY,GAAG,CAAC;CACjE,UAAU;CACV,eAAe,OAAO;CACtB,SAAS,OAAO,MAAM,oBAAoB,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;CACvE,cAAc,OAAO;CACrB,mBAAmB,OAAO;AAC5B,CAAC;AAiED,MAAa,+BAMX,UAEA,OAAO,OAAO;CACZ,OAAO,MAAM;CACb,SAAS,MAAM;CACf,WAAW,MAAM;AACnB,CAAC;AAkBH,MAAa,yBAKX,UAC0D;CAC1D,MAAM,EAAE,OAAO,WAAW,cAAc,UAAU,OAAO,WAAW,WAAW;CAE/E,OAAO,OAAO,OAAO;EACnB,MAAM;EACN;EACA;EACA;EACA;EACA;EACA;EAEA,SAAS,QAAiB,OAAO,GAAkB;CACrD,CAAC;AACH;AAYA,MAAa,qBAKX,UAEA,OAAO,OAAO;CAAE,OAAO,MAAM;CAAO,WAAW,MAAM;CAAW,WAAW,MAAM;AAAU,CAAC;AAmB9F,MAAa,iCAAiC;CAC5C,MAAM;CACN,aAAa;CACb,WAAW;CACX,cAAc;CACd,oBAAoB;CACpB,qBAAqB;AACvB;AAEA,MAAa,uCAAuC;CAClD,iBAAiB;CACjB,YAAY;CACZ,WAAW;CACX,cAAc;CACd,oBAAoB;CACpB,qBAAqB;AACvB"}
@@ -1,4 +1,4 @@
1
- import { anySqlCondition, exactSqlText } from "../sql-change.mjs";
1
+ import { anySqlCondition, exactSqlText, sqlTextBytes } from "../sql-change.mjs";
2
2
  import { CurrentSqlCommit, SqlBatchCommit, makeSqlCommitExecutor } from "../sql-commit.mjs";
3
3
  import { makeConditionalSqlInsert } from "../session-native-record.mjs";
4
4
  import { OAuthEligibilityFact } from "../models/oauth-model.mjs";
@@ -35,6 +35,47 @@ const makeNativeOAuthAccountsServices = Effect.fnUntraced(function* (tables, map
35
35
  tables.expression(c.activeCondition)
36
36
  ]);
37
37
  return { oauthAccountsPersistence: {
38
+ list: (original) => read(Effect.gen(function* () {
39
+ const input = M.snapshotOAuthSync(M.OAuthLinkedAccountsRead, original);
40
+ const nativeId = yield* mapping.subjectId.toNative(input.invocation.subjectId);
41
+ const metadataAccess = tables.expression(mapping.metadataAccess({
42
+ invocation: input.invocation,
43
+ moduleId: input.moduleId,
44
+ subjectId: nativeId
45
+ }));
46
+ const s = mapping.subject;
47
+ const orderedId = yield* sqlTextBytes(credential.column(c.credentialId));
48
+ const cursorCondition = input.cursor === void 0 ? sql`` : sql`and ${orderedId} > ${yield* sqlTextBytes(credential.value(c.credentialId, input.cursor))}`;
49
+ const rows = yield* sql`select ${credential.fields("page_")} from ${credential.name}
50
+ where ${exactSqlText(sql, credential.column(c.moduleId), credential.value(c.moduleId, input.moduleId))}
51
+ and ${credential.column(c.subjectId)} = ${credential.value(c.subjectId, nativeId)}
52
+ and ${tables.expression(c.activeCondition)} and ${metadataAccess}
53
+ and exists(select 1 from ${state.subject.name}
54
+ where ${state.subject.column(s.id)} = ${state.subject.value(s.id, nativeId)}
55
+ and ${tables.expression(s.activeCondition)})
56
+ ${cursorCondition}
57
+ order by ${orderedId} limit ${input.limit + 1}`;
58
+ const selected = rows.slice(0, input.limit);
59
+ const items = [];
60
+ let cursor;
61
+ for (const row of selected) {
62
+ const credentialId = yield* Schema.decodeUnknownEffect(M.OAuthLinkedAccount.fields.credentialId)(credential.decode(row, "page_")[c.credentialId]);
63
+ cursor = credentialId;
64
+ const current = yield* state.readCredential({
65
+ moduleId: input.moduleId,
66
+ subjectId: input.invocation.subjectId,
67
+ credentialId
68
+ }, metadataAccess);
69
+ if (current !== void 0) items.push(M.OAuthLinkedAccount.make({
70
+ credentialId: current.credentialId,
71
+ ...current.identity
72
+ }));
73
+ }
74
+ return M.snapshotOAuthSync(M.OAuthLinkedAccountsListResult, {
75
+ items,
76
+ ...rows.length > input.limit && cursor !== void 0 ? { cursor } : {}
77
+ });
78
+ })),
38
79
  capture: (input) => read(Effect.map(state.capture(input.subjectId, false), (current) => current?.revision)),
39
80
  issue: (original, prepare) => run(Effect.gen(function* () {
40
81
  const input = M.snapshotOAuthSync(M.OAuthLinkFlow, original);