@yielded/auth-persistence 0.1.0-beta.29 → 0.1.0-beta.31
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 +6 -6
- package/dist/Adapter.d.mts +1 -1
- package/dist/Testing.d.mts +44 -0
- package/dist/Testing.mjs +310 -0
- package/dist/Testing.mjs.map +1 -0
- package/dist/internal/configuration.d.mts +18 -8
- package/dist/internal/configuration.mjs.map +1 -1
- package/dist/internal/models/oauth-model.d.mts +10 -1
- 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.mjs +42 -1
- package/dist/internal/oauth/native-accounts.mjs.map +1 -1
- package/dist/internal/oauth/native-state.d.mts +1 -0
- package/dist/internal/oauth/native-state.mjs +2 -2
- package/dist/internal/oauth/native-state.mjs.map +1 -1
- package/dist/internal/passkey-managed.mjs +4 -2
- package/dist/internal/passkey-managed.mjs.map +1 -1
- package/dist/internal/password-native.mjs +14 -0
- package/dist/internal/password-native.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 +4 -6
- package/dist/internal/persistence.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 +2 -1
- package/dist/internal/session-native-authority.mjs.map +1 -1
- package/dist/internal/session-native-login.mjs +2 -1
- package/dist/internal/session-native-login.mjs.map +1 -1
- package/dist/internal/session-native-pending-read.mjs +1 -1
- package/dist/internal/session-native-pending-read.mjs.map +1 -1
- package/dist/internal/session-native-state.mjs +21 -8
- package/dist/internal/session-native-state.mjs.map +1 -1
- package/dist/internal/session-native-stateful.mjs +2 -1
- package/dist/internal/session-native-stateful.mjs.map +1 -1
- package/dist/internal/session-native-step-up.mjs +4 -2
- package/dist/internal/session-native-step-up.mjs.map +1 -1
- package/dist/internal/sql-change.mjs +11 -1
- package/dist/internal/sql-change.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/storage-mapping.d.mts +9 -5
- package/dist/internal/storage-mapping.mjs +16 -4
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Testing.ts +652 -0
- package/src/internal/configuration.ts +26 -8
- package/src/internal/models/oauth-model.ts +10 -0
- package/src/internal/models/password-model.ts +2 -1
- package/src/internal/oauth/native-accounts.ts +72 -1
- package/src/internal/oauth/native-state.ts +6 -2
- package/src/internal/passkey-managed.ts +8 -2
- package/src/internal/password-native.ts +28 -2
- package/src/internal/persistence.ts +174 -164
- package/src/internal/registration.ts +53 -40
- package/src/internal/session-native-authority.ts +3 -1
- package/src/internal/session-native-login.ts +3 -1
- package/src/internal/session-native-pending-read.ts +1 -1
- package/src/internal/session-native-state.ts +29 -10
- package/src/internal/session-native-stateful.ts +3 -1
- package/src/internal/session-native-step-up.ts +5 -2
- package/src/internal/sql-change.ts +14 -1
- package/src/internal/sqlite-policy-guard.ts +34 -0
- package/src/internal/storage-mapping.ts +40 -4
package/README.md
CHANGED
|
@@ -29,11 +29,11 @@ connection and migration runner.
|
|
|
29
29
|
|
|
30
30
|
The composed Layer covers password sign-in and management, email address verification
|
|
31
31
|
and changes, phone sign-in, and passkey sign-in and management with stateful sessions
|
|
32
|
-
on PostgreSQL and SQLite.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
physical columns and expressions and supply transaction or atomic-batch execution.
|
|
36
|
-
Plain reads use no transaction. Protected mutations retain their named final checks
|
|
37
|
-
when an application joins additional work to the owner.
|
|
32
|
+
on PostgreSQL and SQLite. Adapter authors use typed table mappings or
|
|
33
|
+
`NativeSqlTables` through `/Adapter`. Use the explicit coordinators when auth and
|
|
34
|
+
application writes must share one commit; independent transaction owners are not atomic.
|
|
38
35
|
See [persistence examples](../../docs/src/content/docs/reference/adapters.md#runnable-examples) for all
|
|
39
36
|
four ownership models and their current limits.
|
|
37
|
+
|
|
38
|
+
The opt-in [`Testing` module](../../docs/src/content/docs/guide/storage.mdx#in-memory-tests)
|
|
39
|
+
provides non-durable password and session storage with portable crypto services for tests.
|
package/dist/Adapter.d.mts
CHANGED
|
@@ -14,11 +14,11 @@ import { OAuthProxySqlTable, makeOAuthProxyPersistence } from "./internal/oauth-
|
|
|
14
14
|
import { StorageRole, StorageTable, storageTables } from "./internal/storage-tables.mjs";
|
|
15
15
|
import { BoundPersistence, ClaimsCodec, Definition, MappingInput, PersistenceApi, PersistenceConfigurationError } from "./internal/configuration.mjs";
|
|
16
16
|
import { NativeSqlTables, SqlTable, makeNativeSqlTables, nativeSqlAlias } from "./internal/native-sql-table.mjs";
|
|
17
|
+
import { SqlBatchCommit, SqlNativeCommit, captureSqlBatchStatements, makeSqlCommitExecutor } from "./internal/sql-commit.mjs";
|
|
17
18
|
import { Backend, createPersistence } from "./internal/persistence.mjs";
|
|
18
19
|
import { makeMappings } from "./internal/storage-mapping.mjs";
|
|
19
20
|
import { PasswordRegistrationAuthority } from "./internal/registration-contract.mjs";
|
|
20
21
|
import { PhysicalStorageTable, StorageValidation, validateStorage, validateStorageBatch, withStorageValidation } from "./internal/storage-validation.mjs";
|
|
21
|
-
import { SqlBatchCommit, SqlNativeCommit, captureSqlBatchStatements, makeSqlCommitExecutor } from "./internal/sql-commit.mjs";
|
|
22
22
|
import { PasskeyNativeMapping, PasskeyNativeRead } from "./internal/passkey-native-state.mjs";
|
|
23
23
|
import { makeBatchPasskeyServices, makeNativePasskeyCredentialServices, makeNativePasskeyServices } from "./internal/passkey-native.mjs";
|
|
24
24
|
import { NativePasskeyCeremonyMapping, makeNativePasskeyCeremonyServices } from "./internal/passkey-native-ceremony.mjs";
|
|
@@ -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
|
package/dist/Testing.mjs
ADDED
|
@@ -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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"}
|
|
@@ -78,6 +78,19 @@ export interface ProvisioningId<Id extends string> {
|
|
|
78
78
|
readonly authNamespace: Id;
|
|
79
79
|
readonly service: "PersistenceProvisioning";
|
|
80
80
|
}
|
|
81
|
+
export interface SubjectProvisioningInput<Registration> {
|
|
82
|
+
readonly requestId: string;
|
|
83
|
+
readonly identifier: LoginIdentifier;
|
|
84
|
+
readonly registration: Registration;
|
|
85
|
+
}
|
|
86
|
+
/** Return application subject values for insertion by the auth commit owner.
|
|
87
|
+
* Include the allocated native ID, active status, and initial security revision.
|
|
88
|
+
* Preparing values may allocate IDs, but must not write to the database or perform
|
|
89
|
+
* external side effects. Interactive drivers also accept an existing insert callback.
|
|
90
|
+
*/
|
|
91
|
+
export type SubjectProvisioning<Registration> = {
|
|
92
|
+
readonly values: (input: SubjectProvisioningInput<Registration>) => Effect.Effect<Readonly<Record<string, unknown>>, PasswordUnavailable>;
|
|
93
|
+
} | ((input: SubjectProvisioningInput<Registration>) => Effect.Effect<SubjectId, PasswordUnavailable>);
|
|
81
94
|
export type Provisioning<A extends {
|
|
82
95
|
readonly strategies: Readonly<Record<string, Strategy>>;
|
|
83
96
|
}> = { readonly [K in keyof A["strategies"] as A["strategies"][K] extends {
|
|
@@ -85,11 +98,7 @@ export type Provisioning<A extends {
|
|
|
85
98
|
kind: "password";
|
|
86
99
|
management: true;
|
|
87
100
|
};
|
|
88
|
-
} ? K : never]:
|
|
89
|
-
readonly requestId: string;
|
|
90
|
-
readonly identifier: LoginIdentifier;
|
|
91
|
-
readonly registration: Registration<A["strategies"][K]>;
|
|
92
|
-
}) => Effect.Effect<SubjectId, PasswordUnavailable>; };
|
|
101
|
+
} ? K : never]: SubjectProvisioning<Registration<A["strategies"][K]>>; };
|
|
93
102
|
export type ProvisioningRequirement<A extends {
|
|
94
103
|
readonly namespace: string;
|
|
95
104
|
readonly strategies: Readonly<Record<string, Strategy>>;
|
|
@@ -148,9 +157,10 @@ export interface BoundPersistence<T extends object, R, C extends ClaimsCodec, Id
|
|
|
148
157
|
readonly Config: Context.Service<ConfigId<A["namespace"]>, StorageLayout<T, Roles<C, Id, A>>> & {
|
|
149
158
|
readonly layer: (value: StorageLayout<T, Roles<C, Id, A>>) => Layer.Layer<ConfigId<A["namespace"]>>;
|
|
150
159
|
};
|
|
151
|
-
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
160
|
+
/** Supply subject values through `{ values }` on every driver, including D1.
|
|
161
|
+
* Interactive drivers also accept callbacks that insert through the owning
|
|
162
|
+
* SqlClient and return the new subject ID. Never open an independent transaction
|
|
163
|
+
* or perform external side effects while preparing a subject.
|
|
154
164
|
*/
|
|
155
165
|
readonly Provisioning: Context.Service<ProvisioningRequirement<A>, Provisioning<A>>;
|
|
156
166
|
/** Capture dependencies at acquisition; validate SQL metadata and configure storage
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configuration.mjs","names":[],"sources":["../../src/internal/configuration.ts"],"sourcesContent":["import type { EmailAction, EmailAddressPersistence } from \"@yielded/auth/Email\";\nimport type { LoginIdentifier } from \"@yielded/auth/Identity\";\nimport type {\n PasskeyActionChallenge,\n PasskeyConfig,\n PasskeyCredentials,\n PasskeyManagementPersistence,\n PasskeyManagementPolicy,\n PasskeyMethodPolicy,\n PasskeyPersistence,\n PasskeyConfigurationError,\n} from \"@yielded/auth/Passkey\";\nimport type {\n PasswordAction,\n PasswordPersistence,\n PasswordUnavailable,\n} from \"@yielded/auth/Password\";\nimport type { PhoneSignInTargets } from \"@yielded/auth/PhoneOtp\";\nimport type { ProofPersistence } from \"@yielded/auth/Proofs\";\nimport type { SubjectId } from \"@yielded/auth/Schema\";\nimport type {\n AuthenticationAuthority,\n AuthenticationRequirement,\n make as makeSessions,\n} from \"@yielded/auth/Sessions\";\nimport { type Context, type Crypto, type Effect, type Layer, Schema } from \"effect\";\nimport type { SqlClient } from \"effect/sql\";\n\nimport type { PersistenceMappingError } from \"./mapping-error\";\nimport type { StorageRole } from \"./storage-tables\";\n\nexport class PersistenceConfigurationError extends Schema.TaggedError<PersistenceConfigurationError>()(\n \"PersistenceConfigurationError\",\n { reason: Schema.String },\n) {}\n\nexport type ClaimsCodec = Schema.Codec<unknown, unknown, never, never>;\n\nexport interface PasskeyFeature {\n readonly kind: \"passkey\";\n readonly moduleId: string;\n readonly policy: Effect.Effect<PasskeyMethodPolicy, PasskeyConfigurationError, PasskeyConfig>;\n readonly management?: boolean;\n readonly managementPolicy?: PasskeyManagementPolicy;\n}\n\nexport interface Strategy {\n readonly strategy: object;\n readonly persistence?:\n | PasskeyFeature\n | {\n readonly kind: \"password\" | \"phone\" | \"email\";\n readonly moduleId: string;\n readonly lifecycle?: boolean;\n readonly management?: boolean;\n readonly addresses?: boolean;\n };\n readonly RegistrationAuthority?: Context.Key<unknown, unknown>;\n}\n\nexport interface Definition<C extends ClaimsCodec, Id extends string> {\n readonly namespace: string;\n readonly claims: C;\n readonly sessionMode?: string | undefined;\n readonly sessions: ReturnType<typeof makeSessions<C, Id>>;\n readonly strategies: Readonly<Record<string, Strategy>>;\n}\n\ntype KeyId<T> = T extends Context.Key<infer Id, infer _Service> ? Id : never;\ntype Enabled<\n A extends { readonly strategies: Readonly<Record<string, Strategy>> },\n Kind extends \"password\" | \"phone\" | \"email\" | \"passkey\",\n> = Extract<A[\"strategies\"][keyof A[\"strategies\"]], { persistence: { kind: Kind } }>;\n\ntype ManagedPassword<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =\n Extract<Enabled<A, \"password\">, { persistence: { management: true } }>;\n\ntype ManagedPasskey<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =\n Extract<Enabled<A, \"passkey\">, { persistence: { management: true } }>;\n\nexport type PasskeyRequirement<\n A extends { readonly strategies: Readonly<Record<string, Strategy>> },\n> = Enabled<A, \"passkey\"> extends never ? never : PasskeyConfig;\n\ntype RegistrationKey<S> = S extends { readonly RegistrationAuthority: infer K } ? K : never;\ntype Registration<S> =\n RegistrationKey<S> extends Context.Key<infer _Id, infer Service>\n ? Service extends { readonly register: (input: infer Input, ...args: never[]) => unknown }\n ? Input extends { readonly registration: infer Value }\n ? Value\n : never\n : never\n : never;\n\nexport interface ProvisioningId<Id extends string> {\n readonly authNamespace: Id;\n readonly service: \"PersistenceProvisioning\";\n}\n\nexport type Provisioning<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> = {\n readonly [\n K in keyof A[\"strategies\"] as A[\"strategies\"][K] extends {\n persistence: { kind: \"password\"; management: true };\n }\n ? K\n : never\n ]: (input: {\n readonly requestId: string;\n readonly identifier: LoginIdentifier;\n readonly registration: Registration<A[\"strategies\"][K]>;\n }) => Effect.Effect<SubjectId, PasswordUnavailable>;\n};\n\nexport type ProvisioningRequirement<\n A extends { readonly namespace: string; readonly strategies: Readonly<Record<string, Strategy>> },\n> = ManagedPassword<A> extends never ? never : ProvisioningId<A[\"namespace\"]>;\n\ntype ProofRoles = \"proofs\";\n\ntype UsesProofs<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =\n | Enabled<A, \"phone\">\n | Enabled<A, \"email\">\n | ManagedPassword<A>;\n\nexport type Ports<C extends ClaimsCodec, Id extends string, A extends Definition<C, Id>> =\n | AuthenticationAuthority\n | KeyId<A[\"sessions\"][\"StatefulSessionPersistence\"]>\n | KeyId<A[\"sessions\"][\"SessionRepository\"]>\n | KeyId<A[\"sessions\"][\"PendingAuthentication\"]>\n | KeyId<A[\"sessions\"][\"SessionStepUpPersistence\"]>\n | KeyId<A[\"sessions\"][\"SessionCleanup\"]>\n | (Enabled<A, \"password\"> extends never ? never : PasswordPersistence)\n | KeyId<RegistrationKey<ManagedPassword<A>>>\n | (Enabled<A, \"passkey\"> extends never ? never : PasskeyPersistence | PasskeyCredentials)\n | (ManagedPasskey<A> extends never ? never : PasskeyManagementPersistence)\n | (Enabled<A, \"email\"> extends never ? never : EmailAddressPersistence)\n | (UsesProofs<A> extends never ? never : ProofPersistence)\n | (Enabled<A, \"phone\"> extends never ? never : PhoneSignInTargets);\n\nexport type Roles<C extends ClaimsCodec, Id extends string, A extends Definition<C, Id>> =\n | \"identifiers\"\n | \"credentials\"\n | \"sessions\"\n | \"pending\"\n | (Enabled<A, \"password\"> extends never ? never : \"passwords\")\n | (Enabled<A, \"email\"> extends never ? never : \"emailCredentials\")\n | (Enabled<A, \"passkey\"> extends never ? never : \"passkeyCredentials\" | \"passkeyFlows\")\n | (UsesProofs<A> extends never ? never : ProofRoles);\n\nexport interface SubjectOptions<T extends object, NativeId> {\n readonly table: T;\n readonly id: string;\n readonly status: string;\n readonly activeValue: unknown;\n readonly securityRevision: string;\n readonly idCodec: Schema.Codec<SubjectId, NativeId>;\n readonly requirements: (\n row: Readonly<Record<string, unknown>>,\n ) => Effect.Effect<AuthenticationRequirement, PersistenceMappingError>;\n /** Defaults to sign-in requirements. Recovery can have a distinct application policy. */\n readonly actionRequirements?: (\n row: Readonly<Record<string, unknown>>,\n action: PasswordAction | EmailAction | PasskeyActionChallenge[\"action\"],\n ) => Effect.Effect<AuthenticationRequirement, PersistenceMappingError>;\n}\n\n/** Codecs at the application-owned subject and timestamp boundary. */\nexport interface SubjectMapping {\n readonly table: object;\n readonly id: string;\n readonly status: string;\n readonly securityRevision: string;\n readonly activeValue: unknown;\n readonly requirements: SubjectOptions<object, unknown>[\"requirements\"];\n readonly actionRequirements: NonNullable<SubjectOptions<object, unknown>[\"actionRequirements\"]>;\n readonly toSubject: (value: unknown) => Effect.Effect<SubjectId, PersistenceMappingError>;\n readonly toNative: (value: SubjectId) => Effect.Effect<unknown, PersistenceMappingError>;\n readonly toSubjectSync: (value: unknown) => SubjectId;\n readonly toNativeSync: (value: SubjectId) => unknown;\n}\n\nexport interface MappingInput {\n readonly tables: Partial<Record<StorageRole, object>>;\n readonly subjects: SubjectMapping;\n readonly encodeInstant: (millis: number) => unknown;\n readonly decodeInstant: (value: unknown) => Effect.Effect<number, PersistenceMappingError>;\n readonly decodeInstantSync: (value: unknown) => number;\n}\n\nexport interface StorageLayout<\n T extends object,\n Role extends StorageRole = StorageRole,\n> extends MappingInput {\n readonly namespace: string;\n readonly schema: Readonly<Record<Role, T>>;\n}\n\nexport interface ConfigId<Id extends string> {\n readonly authNamespace: Id;\n readonly service: \"PersistenceConfig\";\n}\n\nexport interface TimestampOptions<Instant> {\n readonly type: \"text\" | \"integer\";\n readonly codec: Schema.Codec<number, Instant>;\n}\n\nexport interface BoundPersistence<\n T extends object,\n R,\n C extends ClaimsCodec,\n Id extends string,\n A extends Definition<C, Id>,\n> {\n readonly Config: Context.Service<ConfigId<A[\"namespace\"]>, StorageLayout<T, Roles<C, Id, A>>> & {\n readonly layer: (\n value: StorageLayout<T, Roles<C, Id, A>>,\n ) => Layer.Layer<ConfigId<A[\"namespace\"]>>;\n };\n /** Create only the application subject. Runs inside the owning auth SQL transaction;\n * use that SqlClient (including Drizzle over it), and return the new subject ID.\n * Do not perform external side effects or open an independent transaction here.\n */\n readonly Provisioning: Context.Service<ProvisioningRequirement<A>, Provisioning<A>>;\n /** Capture dependencies at acquisition; validate SQL metadata and configure storage\n * once on the first service operation. Initialization failures become that port's\n * unavailable error. Failure or interruption remains cached until layer reacquisition.\n */\n readonly layer: Layer.Layer<\n Ports<C, Id, A>,\n PersistenceConfigurationError,\n | Crypto.Crypto\n | ConfigId<A[\"namespace\"]>\n | ProvisioningRequirement<A>\n | PasskeyRequirement<A>\n | SqlClient.SqlClient\n | R\n >;\n readonly managed: <N, Instant = number>(options: {\n readonly subjects: SubjectOptions<T, N>;\n readonly tables?: Partial<Record<Roles<C, Id, A>, T>>;\n /** Stable SQL identifier prefix; reuse the deployed prefix for existing tables. */\n readonly prefix: string;\n readonly timestamps?: TimestampOptions<Instant>;\n }) => StorageLayout<T, Roles<C, Id, A>>;\n readonly map: <N, Instant = number>(options: {\n readonly subjects: SubjectOptions<T, N>;\n readonly tables: Readonly<Record<Roles<C, Id, A>, T>>;\n readonly timestamps?: TimestampOptions<Instant>;\n }) => StorageLayout<T, Roles<C, Id, A>>;\n}\n\n/** Shared configuration shape. The backend chooses table types and service requirements. */\nexport interface PersistenceApi<T extends object, R = never> {\n readonly make: <\n C extends ClaimsCodec,\n const Id extends string,\n const A extends Definition<C, Id>,\n >(\n auth: A & Definition<C, Id>,\n ) => BoundPersistence<T, R, C, Id, A>;\n}\n"],"mappings":";;AA+BA,IAAa,gCAAb,cAAmD,OAAO,YAA2C,CAAC,CACpG,iCACA,EAAE,QAAQ,OAAO,OAAO,CAC1B,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"configuration.mjs","names":[],"sources":["../../src/internal/configuration.ts"],"sourcesContent":["import type { EmailAction, EmailAddressPersistence } from \"@yielded/auth/Email\";\nimport type { LoginIdentifier } from \"@yielded/auth/Identity\";\nimport type {\n PasskeyActionChallenge,\n PasskeyConfig,\n PasskeyCredentials,\n PasskeyManagementPersistence,\n PasskeyManagementPolicy,\n PasskeyMethodPolicy,\n PasskeyPersistence,\n PasskeyConfigurationError,\n} from \"@yielded/auth/Passkey\";\nimport type {\n PasswordAction,\n PasswordPersistence,\n PasswordUnavailable,\n} from \"@yielded/auth/Password\";\nimport type { PhoneSignInTargets } from \"@yielded/auth/PhoneOtp\";\nimport type { ProofPersistence } from \"@yielded/auth/Proofs\";\nimport type { SubjectId } from \"@yielded/auth/Schema\";\nimport type {\n AuthenticationAuthority,\n AuthenticationRequirement,\n make as makeSessions,\n} from \"@yielded/auth/Sessions\";\nimport { type Context, type Crypto, type Effect, type Layer, Schema } from \"effect\";\nimport type { SqlClient } from \"effect/sql\";\n\nimport type { PersistenceMappingError } from \"./mapping-error\";\nimport type { StorageRole } from \"./storage-tables\";\n\nexport class PersistenceConfigurationError extends Schema.TaggedError<PersistenceConfigurationError>()(\n \"PersistenceConfigurationError\",\n { reason: Schema.String },\n) {}\n\nexport type ClaimsCodec = Schema.Codec<unknown, unknown, never, never>;\n\nexport interface PasskeyFeature {\n readonly kind: \"passkey\";\n readonly moduleId: string;\n readonly policy: Effect.Effect<PasskeyMethodPolicy, PasskeyConfigurationError, PasskeyConfig>;\n readonly management?: boolean;\n readonly managementPolicy?: PasskeyManagementPolicy;\n}\n\nexport interface Strategy {\n readonly strategy: object;\n readonly persistence?:\n | PasskeyFeature\n | {\n readonly kind: \"password\" | \"phone\" | \"email\";\n readonly moduleId: string;\n readonly lifecycle?: boolean;\n readonly management?: boolean;\n readonly addresses?: boolean;\n };\n readonly RegistrationAuthority?: Context.Key<unknown, unknown>;\n}\n\nexport interface Definition<C extends ClaimsCodec, Id extends string> {\n readonly namespace: string;\n readonly claims: C;\n readonly sessionMode?: string | undefined;\n readonly sessions: ReturnType<typeof makeSessions<C, Id>>;\n readonly strategies: Readonly<Record<string, Strategy>>;\n}\n\ntype KeyId<T> = T extends Context.Key<infer Id, infer _Service> ? Id : never;\ntype Enabled<\n A extends { readonly strategies: Readonly<Record<string, Strategy>> },\n Kind extends \"password\" | \"phone\" | \"email\" | \"passkey\",\n> = Extract<A[\"strategies\"][keyof A[\"strategies\"]], { persistence: { kind: Kind } }>;\n\ntype ManagedPassword<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =\n Extract<Enabled<A, \"password\">, { persistence: { management: true } }>;\n\ntype ManagedPasskey<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =\n Extract<Enabled<A, \"passkey\">, { persistence: { management: true } }>;\n\nexport type PasskeyRequirement<\n A extends { readonly strategies: Readonly<Record<string, Strategy>> },\n> = Enabled<A, \"passkey\"> extends never ? never : PasskeyConfig;\n\ntype RegistrationKey<S> = S extends { readonly RegistrationAuthority: infer K } ? K : never;\ntype Registration<S> =\n RegistrationKey<S> extends Context.Key<infer _Id, infer Service>\n ? Service extends { readonly register: (input: infer Input, ...args: never[]) => unknown }\n ? Input extends { readonly registration: infer Value }\n ? Value\n : never\n : never\n : never;\n\nexport interface ProvisioningId<Id extends string> {\n readonly authNamespace: Id;\n readonly service: \"PersistenceProvisioning\";\n}\n\nexport interface SubjectProvisioningInput<Registration> {\n readonly requestId: string;\n readonly identifier: LoginIdentifier;\n readonly registration: Registration;\n}\n\n/** Return application subject values for insertion by the auth commit owner.\n * Include the allocated native ID, active status, and initial security revision.\n * Preparing values may allocate IDs, but must not write to the database or perform\n * external side effects. Interactive drivers also accept an existing insert callback.\n */\nexport type SubjectProvisioning<Registration> =\n | {\n readonly values: (\n input: SubjectProvisioningInput<Registration>,\n ) => Effect.Effect<Readonly<Record<string, unknown>>, PasswordUnavailable>;\n }\n | ((\n input: SubjectProvisioningInput<Registration>,\n ) => Effect.Effect<SubjectId, PasswordUnavailable>);\n\nexport type Provisioning<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> = {\n readonly [\n K in keyof A[\"strategies\"] as A[\"strategies\"][K] extends {\n persistence: { kind: \"password\"; management: true };\n }\n ? K\n : never\n ]: SubjectProvisioning<Registration<A[\"strategies\"][K]>>;\n};\n\nexport type ProvisioningRequirement<\n A extends { readonly namespace: string; readonly strategies: Readonly<Record<string, Strategy>> },\n> = ManagedPassword<A> extends never ? never : ProvisioningId<A[\"namespace\"]>;\n\ntype ProofRoles = \"proofs\";\n\ntype UsesProofs<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =\n | Enabled<A, \"phone\">\n | Enabled<A, \"email\">\n | ManagedPassword<A>;\n\nexport type Ports<C extends ClaimsCodec, Id extends string, A extends Definition<C, Id>> =\n | AuthenticationAuthority\n | KeyId<A[\"sessions\"][\"StatefulSessionPersistence\"]>\n | KeyId<A[\"sessions\"][\"SessionRepository\"]>\n | KeyId<A[\"sessions\"][\"PendingAuthentication\"]>\n | KeyId<A[\"sessions\"][\"SessionStepUpPersistence\"]>\n | KeyId<A[\"sessions\"][\"SessionCleanup\"]>\n | (Enabled<A, \"password\"> extends never ? never : PasswordPersistence)\n | KeyId<RegistrationKey<ManagedPassword<A>>>\n | (Enabled<A, \"passkey\"> extends never ? never : PasskeyPersistence | PasskeyCredentials)\n | (ManagedPasskey<A> extends never ? never : PasskeyManagementPersistence)\n | (Enabled<A, \"email\"> extends never ? never : EmailAddressPersistence)\n | (UsesProofs<A> extends never ? never : ProofPersistence)\n | (Enabled<A, \"phone\"> extends never ? never : PhoneSignInTargets);\n\nexport type Roles<C extends ClaimsCodec, Id extends string, A extends Definition<C, Id>> =\n | \"identifiers\"\n | \"credentials\"\n | \"sessions\"\n | \"pending\"\n | (Enabled<A, \"password\"> extends never ? never : \"passwords\")\n | (Enabled<A, \"email\"> extends never ? never : \"emailCredentials\")\n | (Enabled<A, \"passkey\"> extends never ? never : \"passkeyCredentials\" | \"passkeyFlows\")\n | (UsesProofs<A> extends never ? never : ProofRoles);\n\nexport interface SubjectOptions<T extends object, NativeId> {\n readonly table: T;\n readonly id: string;\n readonly status: string;\n readonly activeValue: unknown;\n readonly securityRevision: string;\n readonly idCodec: Schema.Codec<SubjectId, NativeId>;\n readonly requirements: (\n row: Readonly<Record<string, unknown>>,\n ) => Effect.Effect<AuthenticationRequirement, PersistenceMappingError>;\n /** Defaults to sign-in requirements. Recovery can have a distinct application policy. */\n readonly actionRequirements?: (\n row: Readonly<Record<string, unknown>>,\n action: PasswordAction | EmailAction | PasskeyActionChallenge[\"action\"],\n ) => Effect.Effect<AuthenticationRequirement, PersistenceMappingError>;\n}\n\n/** Codecs at the application-owned subject and timestamp boundary. */\nexport interface SubjectMapping {\n readonly table: object;\n readonly id: string;\n readonly status: string;\n readonly securityRevision: string;\n readonly activeValue: unknown;\n readonly requirements: SubjectOptions<object, unknown>[\"requirements\"];\n readonly actionRequirements: NonNullable<SubjectOptions<object, unknown>[\"actionRequirements\"]>;\n readonly toSubject: (value: unknown) => Effect.Effect<SubjectId, PersistenceMappingError>;\n readonly toNative: (value: SubjectId) => Effect.Effect<unknown, PersistenceMappingError>;\n readonly toSubjectSync: (value: unknown) => SubjectId;\n readonly toNativeSync: (value: SubjectId) => unknown;\n}\n\nexport interface MappingInput {\n readonly tables: Partial<Record<StorageRole, object>>;\n readonly subjects: SubjectMapping;\n readonly encodeInstant: (millis: number) => unknown;\n readonly decodeInstant: (value: unknown) => Effect.Effect<number, PersistenceMappingError>;\n readonly decodeInstantSync: (value: unknown) => number;\n}\n\nexport interface StorageLayout<\n T extends object,\n Role extends StorageRole = StorageRole,\n> extends MappingInput {\n readonly namespace: string;\n readonly schema: Readonly<Record<Role, T>>;\n}\n\nexport interface ConfigId<Id extends string> {\n readonly authNamespace: Id;\n readonly service: \"PersistenceConfig\";\n}\n\nexport interface TimestampOptions<Instant> {\n readonly type: \"text\" | \"integer\";\n readonly codec: Schema.Codec<number, Instant>;\n}\n\nexport interface BoundPersistence<\n T extends object,\n R,\n C extends ClaimsCodec,\n Id extends string,\n A extends Definition<C, Id>,\n> {\n readonly Config: Context.Service<ConfigId<A[\"namespace\"]>, StorageLayout<T, Roles<C, Id, A>>> & {\n readonly layer: (\n value: StorageLayout<T, Roles<C, Id, A>>,\n ) => Layer.Layer<ConfigId<A[\"namespace\"]>>;\n };\n /** Supply subject values through `{ values }` on every driver, including D1.\n * Interactive drivers also accept callbacks that insert through the owning\n * SqlClient and return the new subject ID. Never open an independent transaction\n * or perform external side effects while preparing a subject.\n */\n readonly Provisioning: Context.Service<ProvisioningRequirement<A>, Provisioning<A>>;\n /** Capture dependencies at acquisition; validate SQL metadata and configure storage\n * once on the first service operation. Initialization failures become that port's\n * unavailable error. Failure or interruption remains cached until layer reacquisition.\n */\n readonly layer: Layer.Layer<\n Ports<C, Id, A>,\n PersistenceConfigurationError,\n | Crypto.Crypto\n | ConfigId<A[\"namespace\"]>\n | ProvisioningRequirement<A>\n | PasskeyRequirement<A>\n | SqlClient.SqlClient\n | R\n >;\n readonly managed: <N, Instant = number>(options: {\n readonly subjects: SubjectOptions<T, N>;\n readonly tables?: Partial<Record<Roles<C, Id, A>, T>>;\n /** Stable SQL identifier prefix; reuse the deployed prefix for existing tables. */\n readonly prefix: string;\n readonly timestamps?: TimestampOptions<Instant>;\n }) => StorageLayout<T, Roles<C, Id, A>>;\n readonly map: <N, Instant = number>(options: {\n readonly subjects: SubjectOptions<T, N>;\n readonly tables: Readonly<Record<Roles<C, Id, A>, T>>;\n readonly timestamps?: TimestampOptions<Instant>;\n }) => StorageLayout<T, Roles<C, Id, A>>;\n}\n\n/** Shared configuration shape. The backend chooses table types and service requirements. */\nexport interface PersistenceApi<T extends object, R = never> {\n readonly make: <\n C extends ClaimsCodec,\n const Id extends string,\n const A extends Definition<C, Id>,\n >(\n auth: A & Definition<C, Id>,\n ) => BoundPersistence<T, R, C, Id, A>;\n}\n"],"mappings":";;AA+BA,IAAa,gCAAb,cAAmD,OAAO,YAA2C,CAAC,CACpG,iCACA,EAAE,QAAQ,OAAO,OAAO,CAC1B,CAAC,CAAC,CAAC"}
|
|
@@ -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";
|