@yielded/auth-persistence 0.1.0-beta.23 → 0.1.0-beta.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/Adapter.d.mts +12 -6
- package/dist/Adapter.mjs +8 -3
- package/dist/OAuthPersistence.d.mts +87 -0
- package/dist/OAuthPersistence.mjs +125 -0
- package/dist/OAuthPersistence.mjs.map +1 -0
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/dist/internal/configuration.d.mts +1 -1
- package/dist/internal/configuration.mjs.map +1 -1
- package/dist/internal/models/oauth-connected-model.d.mts +294 -0
- package/dist/internal/models/oauth-connected-model.mjs +22 -0
- package/dist/internal/models/oauth-connected-model.mjs.map +1 -0
- package/dist/internal/models/oauth-model.d.mts +467 -0
- package/dist/internal/models/oauth-model.mjs +49 -0
- package/dist/internal/models/oauth-model.mjs.map +1 -0
- package/dist/internal/models/password-model.d.mts +4 -47
- package/dist/internal/models/password-model.mjs +0 -1
- package/dist/internal/models/password-model.mjs.map +1 -1
- package/dist/internal/native-sql-table.d.mts +4 -0
- package/dist/internal/native-sql-table.mjs +7 -0
- package/dist/internal/native-sql-table.mjs.map +1 -1
- package/dist/internal/oauth/accounts.d.mts +8 -0
- package/dist/internal/oauth/accounts.mjs +379 -0
- package/dist/internal/oauth/accounts.mjs.map +1 -0
- package/dist/internal/oauth/connected-access.d.mts +9 -0
- package/dist/internal/oauth/connected-access.mjs +229 -0
- package/dist/internal/oauth/connected-access.mjs.map +1 -0
- package/dist/internal/oauth/connected-collection.d.mts +7 -0
- package/dist/internal/oauth/connected-collection.mjs +214 -0
- package/dist/internal/oauth/connected-collection.mjs.map +1 -0
- package/dist/internal/oauth/connected-custody.d.mts +6 -0
- package/dist/internal/oauth/connected-custody.mjs +165 -0
- package/dist/internal/oauth/connected-custody.mjs.map +1 -0
- package/dist/internal/oauth/connected-flow.d.mts +7 -0
- package/dist/internal/oauth/connected-flow.mjs +324 -0
- package/dist/internal/oauth/connected-flow.mjs.map +1 -0
- package/dist/internal/oauth/connected-input.mjs +122 -0
- package/dist/internal/oauth/connected-input.mjs.map +1 -0
- package/dist/internal/oauth/connected-keys.d.mts +2 -0
- package/dist/internal/oauth/connected-keys.mjs +22 -0
- package/dist/internal/oauth/connected-keys.mjs.map +1 -0
- package/dist/internal/oauth/connected-maintenance.d.mts +9 -0
- package/dist/internal/oauth/connected-maintenance.mjs +226 -0
- package/dist/internal/oauth/connected-maintenance.mjs.map +1 -0
- package/dist/internal/oauth/connected-management.d.mts +9 -0
- package/dist/internal/oauth/connected-management.mjs +243 -0
- package/dist/internal/oauth/connected-management.mjs.map +1 -0
- package/dist/internal/oauth/connected-reference.mjs +63 -0
- package/dist/internal/oauth/connected-reference.mjs.map +1 -0
- package/dist/internal/oauth/connected-settlement.d.mts +8 -0
- package/dist/internal/oauth/connected-settlement.mjs +141 -0
- package/dist/internal/oauth/connected-settlement.mjs.map +1 -0
- package/dist/internal/oauth/connected-sign-in.d.mts +10 -0
- package/dist/internal/oauth/connected-sign-in.mjs +281 -0
- package/dist/internal/oauth/connected-sign-in.mjs.map +1 -0
- package/dist/internal/oauth/connected-state.d.mts +14 -0
- package/dist/internal/oauth/connected-state.mjs +469 -0
- package/dist/internal/oauth/connected-state.mjs.map +1 -0
- package/dist/internal/oauth/connected-target.d.mts +21 -0
- package/dist/internal/oauth/connected-target.mjs +117 -0
- package/dist/internal/oauth/connected-target.mjs.map +1 -0
- package/dist/internal/oauth/flow.d.mts +5 -0
- package/dist/internal/oauth/flow.mjs +279 -0
- package/dist/internal/oauth/flow.mjs.map +1 -0
- package/dist/internal/oauth/input.mjs +127 -0
- package/dist/internal/oauth/input.mjs.map +1 -0
- package/dist/internal/oauth/owner.d.mts +10 -0
- package/dist/internal/oauth/owner.mjs +10 -0
- package/dist/internal/oauth/owner.mjs.map +1 -0
- package/dist/internal/oauth/query-compiler.d.mts +7 -0
- package/dist/internal/oauth/query-compiler.mjs +9 -0
- package/dist/internal/oauth/query-compiler.mjs.map +1 -0
- package/dist/internal/oauth/registration.d.mts +7 -0
- package/dist/internal/oauth/registration.mjs +393 -0
- package/dist/internal/oauth/registration.mjs.map +1 -0
- package/dist/internal/oauth/state.mjs +107 -0
- package/dist/internal/oauth/state.mjs.map +1 -0
- package/dist/internal/oauth/target.d.mts +31 -0
- package/dist/internal/oauth/target.mjs +273 -0
- package/dist/internal/oauth/target.mjs.map +1 -0
- package/dist/internal/oauth-kernel.d.mts +1423 -0
- package/dist/internal/oauth-kernel.mjs +187 -0
- package/dist/internal/oauth-kernel.mjs.map +1 -0
- package/dist/internal/oauth-proxy.d.mts +31 -0
- package/dist/internal/oauth-proxy.mjs +129 -0
- package/dist/internal/oauth-proxy.mjs.map +1 -0
- package/dist/internal/passkey-kernel.d.mts +5 -5
- package/dist/internal/{password-attempts.d.mts → password-credentials.d.mts} +3 -4
- package/dist/internal/password-credentials.mjs +121 -0
- package/dist/internal/password-credentials.mjs.map +1 -0
- package/dist/internal/password-kernel.d.mts +8 -24
- package/dist/internal/password-kernel.mjs +4 -42
- package/dist/internal/password-kernel.mjs.map +1 -1
- package/dist/internal/persistence.d.mts +1 -1
- package/dist/internal/persistence.mjs +2 -2
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/query-operations.d.mts +2 -0
- package/dist/internal/session-kernel.d.mts +2 -2
- package/dist/internal/session-kernel.mjs +74 -18
- package/dist/internal/session-kernel.mjs.map +1 -1
- package/dist/internal/sql-oauth-connected-model.d.mts +29 -0
- package/dist/internal/sql-oauth-connected-model.mjs +7 -0
- package/dist/internal/sql-oauth-connected-model.mjs.map +1 -0
- package/dist/internal/sql-oauth-model.d.mts +60 -0
- package/dist/internal/sql-oauth-model.mjs +10 -0
- package/dist/internal/sql-oauth-model.mjs.map +1 -0
- package/dist/internal/sql-query.d.mts +12 -0
- package/dist/internal/sql-query.mjs +15 -18
- package/dist/internal/sql-query.mjs.map +1 -1
- package/dist/internal/sql-storage-validation.mjs +54 -0
- package/dist/internal/sql-storage-validation.mjs.map +1 -0
- package/dist/internal/storage-mapping.mjs +1 -37
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/dist/internal/storage-plans.d.mts +7 -0
- package/dist/internal/storage-plans.mjs +189 -0
- package/dist/internal/storage-plans.mjs.map +1 -0
- package/dist/internal/storage-tables.d.mts +0 -56
- package/dist/internal/storage-tables.mjs +0 -17
- package/dist/internal/storage-tables.mjs.map +1 -1
- package/dist/internal/storage-validation.d.mts +6 -1
- package/dist/internal/storage-validation.mjs +42 -1
- package/dist/internal/storage-validation.mjs.map +1 -1
- package/dist/internal/transaction-kernel.mjs +3 -2
- package/dist/internal/transaction-kernel.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Adapter.ts +89 -5
- package/src/OAuthPersistence.ts +427 -0
- package/src/index.ts +3 -0
- package/src/internal/configuration.ts +1 -3
- package/src/internal/models/oauth-connected-model.ts +396 -0
- package/src/internal/models/oauth-model.ts +708 -0
- package/src/internal/models/password-model.ts +0 -56
- package/src/internal/native-sql-table.ts +14 -0
- package/src/internal/oauth/accounts.ts +838 -0
- package/src/internal/oauth/connected-access.ts +388 -0
- package/src/internal/oauth/connected-collection.ts +345 -0
- package/src/internal/oauth/connected-custody.ts +308 -0
- package/src/internal/oauth/connected-flow.ts +562 -0
- package/src/internal/oauth/connected-input.ts +147 -0
- package/src/internal/oauth/connected-keys.ts +31 -0
- package/src/internal/oauth/connected-maintenance.ts +379 -0
- package/src/internal/oauth/connected-management.ts +391 -0
- package/src/internal/oauth/connected-reference.ts +135 -0
- package/src/internal/oauth/connected-settlement.ts +278 -0
- package/src/internal/oauth/connected-sign-in.ts +472 -0
- package/src/internal/oauth/connected-state.ts +924 -0
- package/src/internal/oauth/connected-target.ts +307 -0
- package/src/internal/oauth/flow.ts +505 -0
- package/src/internal/oauth/input.ts +201 -0
- package/src/internal/oauth/owner.ts +22 -0
- package/src/internal/oauth/query-compiler.ts +15 -0
- package/src/internal/oauth/registration.ts +676 -0
- package/src/internal/oauth/state.ts +218 -0
- package/src/internal/oauth/target.ts +803 -0
- package/src/internal/oauth-kernel.ts +294 -0
- package/src/internal/oauth-proxy.ts +176 -0
- package/src/internal/password-credentials.ts +245 -0
- package/src/internal/password-kernel.ts +5 -98
- package/src/internal/persistence.ts +1 -1
- package/src/internal/query-operations.ts +2 -0
- package/src/internal/session-kernel.ts +143 -55
- package/src/internal/sql-oauth-connected-model.ts +145 -0
- package/src/internal/sql-oauth-model.ts +263 -0
- package/src/internal/sql-query.ts +16 -13
- package/src/internal/sql-storage-validation.ts +64 -0
- package/src/internal/storage-mapping.ts +0 -37
- package/src/internal/storage-plans.ts +218 -0
- package/src/internal/storage-tables.ts +0 -20
- package/src/internal/storage-validation.ts +58 -0
- package/src/internal/transaction-kernel.ts +6 -2
- package/dist/internal/password-attempts.mjs +0 -197
- package/dist/internal/password-attempts.mjs.map +0 -1
- package/src/internal/password-attempts.ts +0 -466
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
coordinateCommit,
|
|
3
|
-
CurrentCommitJournal,
|
|
4
|
-
hasCommitScope,
|
|
5
|
-
LifecycleHooks,
|
|
6
|
-
} from "@yielded/auth/Hooks";
|
|
7
|
-
import type { LoginIdentifier } from "@yielded/auth/Identity";
|
|
8
|
-
import {
|
|
9
|
-
type PasswordPersistence,
|
|
10
|
-
PasswordUnavailable,
|
|
11
|
-
snapshotPasswordCredential,
|
|
12
|
-
type PasswordCredentialSnapshot,
|
|
13
|
-
type PreparePasswordCommit,
|
|
14
|
-
type PasswordAttemptAdmission,
|
|
15
|
-
} from "@yielded/auth/Password";
|
|
16
|
-
import { reportPersistenceFailure } from "@yielded/auth/Persistence";
|
|
17
|
-
import { DateTime, Effect, Option, Redacted, Schema } from "effect";
|
|
18
|
-
|
|
19
|
-
import type { PersistenceMappingError } from "./mapping-error";
|
|
20
|
-
import type { AnyPasswordPersistenceMapping } from "./models/password-model";
|
|
21
|
-
import { sqlMapping, type SqlTable } from "./native-sql-table";
|
|
22
|
-
import type { PasswordSqlConfiguration } from "./password-kernel";
|
|
23
|
-
import { PasswordPreparedPostconditions } from "./PasswordPreparedPostconditions";
|
|
24
|
-
import { NativeDatabase } from "./transaction-kernel";
|
|
25
|
-
|
|
26
|
-
type Row = Readonly<Record<string, unknown>>;
|
|
27
|
-
type Mapping = AnyPasswordPersistenceMapping;
|
|
28
|
-
|
|
29
|
-
const unavailable = () => PasswordUnavailable.make({});
|
|
30
|
-
const nowMillis = DateTime.now.pipe(Effect.map(DateTime.toEpochMillis));
|
|
31
|
-
|
|
32
|
-
const failure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
33
|
-
reportPersistenceFailure(effect, Schema.is(PasswordUnavailable)).pipe(
|
|
34
|
-
Effect.mapError(unavailable),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
export const samePasswordCredentialSnapshot = (
|
|
38
|
-
a: PasswordCredentialSnapshot,
|
|
39
|
-
b: PasswordCredentialSnapshot,
|
|
40
|
-
) =>
|
|
41
|
-
a.moduleId === b.moduleId &&
|
|
42
|
-
a.revision.subjectId === b.revision.subjectId &&
|
|
43
|
-
a.revision.securityRevision === b.revision.securityRevision &&
|
|
44
|
-
a.revision.credentials.length === b.revision.credentials.length &&
|
|
45
|
-
new Set(a.revision.credentials.map((entry) => entry.credentialId)).size ===
|
|
46
|
-
a.revision.credentials.length &&
|
|
47
|
-
new Set(b.revision.credentials.map((entry) => entry.credentialId)).size ===
|
|
48
|
-
b.revision.credentials.length &&
|
|
49
|
-
a.revision.credentials.every((entry) =>
|
|
50
|
-
b.revision.credentials.some(
|
|
51
|
-
(other) => entry.credentialId === other.credentialId && entry.revision === other.revision,
|
|
52
|
-
),
|
|
53
|
-
) &&
|
|
54
|
-
a.credentialId === b.credentialId &&
|
|
55
|
-
a.credentialRevision === b.credentialRevision &&
|
|
56
|
-
a.verifierVersion === b.verifierVersion &&
|
|
57
|
-
Redacted.value(a.verifier) === Redacted.value(b.verifier) &&
|
|
58
|
-
a.normalization === b.normalization &&
|
|
59
|
-
a.identifier.namespace === b.identifier.namespace &&
|
|
60
|
-
a.identifier.value === b.identifier.value &&
|
|
61
|
-
a.identifierBindingRevision === b.identifierBindingRevision &&
|
|
62
|
-
a.identifierVerifiedAtMillis === b.identifierVerifiedAtMillis;
|
|
63
|
-
|
|
64
|
-
/** The interactive attempt workflow uses the owning Effect SQL client directly.
|
|
65
|
-
* Table adapters supply representation only; admission and settlement policy live here.
|
|
66
|
-
*/
|
|
67
|
-
export const makePasswordAttempts = Effect.fnUntraced(function* (
|
|
68
|
-
table: (table: object) => SqlTable,
|
|
69
|
-
mapping: Mapping,
|
|
70
|
-
options: PasswordSqlConfiguration,
|
|
71
|
-
): Effect.fn.Return<
|
|
72
|
-
Pick<PasswordPersistence["Service"], "prepareAttempt" | "settleAttempt">,
|
|
73
|
-
PasswordUnavailable,
|
|
74
|
-
LifecycleHooks | NativeDatabase
|
|
75
|
-
> {
|
|
76
|
-
const sql = (yield* NativeDatabase).$client.withoutTransforms();
|
|
77
|
-
const hooks = yield* LifecycleHooks;
|
|
78
|
-
const parent = yield* Effect.serviceOption(CurrentCommitJournal);
|
|
79
|
-
|
|
80
|
-
const [s, i, c, a] = yield* sqlMapping(
|
|
81
|
-
() =>
|
|
82
|
-
[
|
|
83
|
-
table(mapping.subject.table),
|
|
84
|
-
table(mapping.identifier.table),
|
|
85
|
-
table(mapping.credential.table),
|
|
86
|
-
table(mapping.attempt.table),
|
|
87
|
-
] as const,
|
|
88
|
-
).pipe(failure);
|
|
89
|
-
|
|
90
|
-
const lock = options.locking ? sql.literal(" FOR UPDATE") : sql.literal("");
|
|
91
|
-
|
|
92
|
-
const standalone = Effect.gen(function* () {
|
|
93
|
-
if (options.coordinated) {
|
|
94
|
-
const current = yield* Effect.serviceOption(CurrentCommitJournal);
|
|
95
|
-
|
|
96
|
-
if (Option.isNone(parent) || Option.isNone(current) || parent.value !== current.value)
|
|
97
|
-
return yield* unavailable();
|
|
98
|
-
} else {
|
|
99
|
-
if (yield* hasCommitScope) return yield* unavailable();
|
|
100
|
-
yield* options.standaloneGuard;
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const owned = <A, E, R>(work: Effect.Effect<A, E, R>) =>
|
|
105
|
-
standalone.pipe(
|
|
106
|
-
Effect.andThen(coordinateCommit(() => sql.withTransaction(work), { mode: options.mode })),
|
|
107
|
-
Effect.map((result) => result.value),
|
|
108
|
-
Effect.provideService(LifecycleHooks, hooks),
|
|
109
|
-
failure,
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const allocate = <A>(
|
|
113
|
-
asynchronous: Effect.Effect<A, PersistenceMappingError> | undefined,
|
|
114
|
-
sync: (() => A) | undefined,
|
|
115
|
-
) => {
|
|
116
|
-
if (options.mode !== "synchronous" && asynchronous !== undefined) return asynchronous;
|
|
117
|
-
|
|
118
|
-
return sync === undefined
|
|
119
|
-
? Effect.fail(unavailable())
|
|
120
|
-
: Effect.try({ try: sync, catch: unavailable });
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Bind each native ID through its own column codec. Physical encodings of the
|
|
124
|
-
// same application ID need not match across subject, identifier and password.
|
|
125
|
-
const snapshot = Effect.fnUntraced(function* (
|
|
126
|
-
nativeId: unknown,
|
|
127
|
-
moduleId: string,
|
|
128
|
-
identifier: LoginIdentifier,
|
|
129
|
-
locking: boolean,
|
|
130
|
-
attemptId?: string,
|
|
131
|
-
) {
|
|
132
|
-
const subjectTable = s.as("password_subject");
|
|
133
|
-
const identifierTable = i.as("password_identifier");
|
|
134
|
-
const credentialTable = c.as("password_credential");
|
|
135
|
-
const attemptTable = a.as("password_attempt");
|
|
136
|
-
|
|
137
|
-
const rows = yield* sqlMapping(
|
|
138
|
-
() =>
|
|
139
|
-
sql<Row>`select ${subjectTable.fields("subject_")}, ${identifierTable.fields("identifier_")}, ${credentialTable.fields("credential_")}
|
|
140
|
-
${attemptId === undefined ? sql.literal("") : sql`, ${attemptTable.fields("attempt_")}`}
|
|
141
|
-
from ${subjectTable.name}
|
|
142
|
-
inner join ${identifierTable.name} on ${identifierTable.column(mapping.identifier.namespace)} = ${identifierTable.value(mapping.identifier.namespace, identifier.namespace)}
|
|
143
|
-
and ${identifierTable.column(mapping.identifier.value)} = ${identifierTable.value(mapping.identifier.value, identifier.value)}
|
|
144
|
-
and ${identifierTable.column(mapping.identifier.subjectId)} = ${identifierTable.value(mapping.identifier.subjectId, nativeId)}
|
|
145
|
-
inner join ${credentialTable.name} on ${credentialTable.column(mapping.credential.moduleId)} = ${credentialTable.value(mapping.credential.moduleId, moduleId)}
|
|
146
|
-
and ${credentialTable.column(mapping.credential.subjectId)} = ${credentialTable.value(mapping.credential.subjectId, nativeId)}
|
|
147
|
-
${
|
|
148
|
-
attemptId === undefined
|
|
149
|
-
? sql.literal("")
|
|
150
|
-
: sql`inner join ${attemptTable.name} on
|
|
151
|
-
${attemptTable.column(mapping.attempt.moduleId)} = ${attemptTable.value(mapping.attempt.moduleId, moduleId)}
|
|
152
|
-
and ${attemptTable.column(mapping.attempt.attemptId)} = ${attemptTable.value(mapping.attempt.attemptId, attemptId)}`
|
|
153
|
-
}
|
|
154
|
-
where ${subjectTable.column(mapping.subject.id)} = ${subjectTable.value(mapping.subject.id, nativeId)}
|
|
155
|
-
${locking ? lock : sql.literal("")}`,
|
|
156
|
-
).pipe(Effect.flatten);
|
|
157
|
-
|
|
158
|
-
if (rows.length !== 1) return undefined;
|
|
159
|
-
|
|
160
|
-
const [subject, identifierRow, credential] = yield* sqlMapping(
|
|
161
|
-
() =>
|
|
162
|
-
[
|
|
163
|
-
s.decode(rows[0]!, "subject_"),
|
|
164
|
-
i.decode(rows[0]!, "identifier_"),
|
|
165
|
-
c.decode(rows[0]!, "credential_"),
|
|
166
|
-
] as const,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
yield* sqlMapping(
|
|
171
|
-
() =>
|
|
172
|
-
!mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
|
|
173
|
-
!mapping.identifier.isCurrent(identifierRow) ||
|
|
174
|
-
!mapping.subjectId.equals(nativeId, identifierRow[mapping.identifier.subjectId]) ||
|
|
175
|
-
!mapping.subjectId.equals(nativeId, subject[mapping.subject.id]) ||
|
|
176
|
-
!mapping.subjectId.equals(nativeId, credential[mapping.credential.subjectId]),
|
|
177
|
-
)
|
|
178
|
-
)
|
|
179
|
-
return undefined;
|
|
180
|
-
|
|
181
|
-
const captured = yield* sqlMapping(() =>
|
|
182
|
-
mapping.credential.decode({ moduleId, subject, identifier: identifierRow, credential }),
|
|
183
|
-
).pipe(Effect.flatten, Effect.flatMap(snapshotPasswordCredential));
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
nativeId,
|
|
187
|
-
captured,
|
|
188
|
-
attempt:
|
|
189
|
-
attemptId === undefined
|
|
190
|
-
? undefined
|
|
191
|
-
: yield* sqlMapping(() => a.decode(rows[0]!, "attempt_")),
|
|
192
|
-
};
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const attemptRow = Effect.fnUntraced(function* (
|
|
196
|
-
moduleId: string,
|
|
197
|
-
attemptId: string,
|
|
198
|
-
locking: boolean,
|
|
199
|
-
) {
|
|
200
|
-
const rows = yield* sqlMapping(
|
|
201
|
-
() => sql<Row>`select ${a.fields("a_")} from ${a.name}
|
|
202
|
-
where ${a.column(mapping.attempt.moduleId)} = ${a.value(mapping.attempt.moduleId, moduleId)}
|
|
203
|
-
and ${a.column(mapping.attempt.attemptId)} = ${a.value(mapping.attempt.attemptId, attemptId)}
|
|
204
|
-
${locking ? lock : sql.literal("")}`,
|
|
205
|
-
).pipe(Effect.flatten);
|
|
206
|
-
|
|
207
|
-
return rows.length === 1 ? yield* sqlMapping(() => a.decode(rows[0]!, "a_")) : undefined;
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const attemptMatches = (row: Row, candidate: PasswordCredentialSnapshot, nativeId: unknown) =>
|
|
211
|
-
row[mapping.attempt.moduleId] === candidate.moduleId &&
|
|
212
|
-
row[mapping.attempt.identifierNamespace] === candidate.identifier.namespace &&
|
|
213
|
-
row[mapping.attempt.identifierValue] === candidate.identifier.value &&
|
|
214
|
-
mapping.subjectId.equals(row[mapping.attempt.subjectId], nativeId) &&
|
|
215
|
-
row[mapping.attempt.credentialId] === candidate.credentialId &&
|
|
216
|
-
row[mapping.attempt.securityRevision] === candidate.revision.securityRevision &&
|
|
217
|
-
row[mapping.attempt.credentialRevision] === candidate.credentialRevision &&
|
|
218
|
-
row[mapping.attempt.verifierVersion] === candidate.verifierVersion &&
|
|
219
|
-
row[mapping.attempt.identifierBindingRevision] === candidate.identifierBindingRevision;
|
|
220
|
-
|
|
221
|
-
const service: Pick<PasswordPersistence["Service"], "prepareAttempt" | "settleAttempt"> = {
|
|
222
|
-
// The remaining operations are composed by the persistence owner.
|
|
223
|
-
prepareAttempt: (uncaptured) =>
|
|
224
|
-
Effect.gen(function* () {
|
|
225
|
-
const input = Object.freeze({
|
|
226
|
-
...uncaptured,
|
|
227
|
-
identifier: Object.freeze({ ...uncaptured.identifier }),
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
yield* standalone;
|
|
231
|
-
|
|
232
|
-
const identifiers = yield* sqlMapping(
|
|
233
|
-
() => sql<Row>`select ${i.fields("i_")} from ${i.name}
|
|
234
|
-
where ${i.column(mapping.identifier.namespace)} = ${i.value(mapping.identifier.namespace, input.identifier.namespace)}
|
|
235
|
-
and ${i.column(mapping.identifier.value)} = ${i.value(mapping.identifier.value, input.identifier.value)}`,
|
|
236
|
-
).pipe(Effect.flatten);
|
|
237
|
-
|
|
238
|
-
const identifier =
|
|
239
|
-
identifiers.length === 1
|
|
240
|
-
? yield* sqlMapping(() => i.decode(identifiers[0]!, "i_"))
|
|
241
|
-
: undefined;
|
|
242
|
-
|
|
243
|
-
const resolved =
|
|
244
|
-
identifier === undefined ||
|
|
245
|
-
!(yield* sqlMapping(() => mapping.identifier.isCurrent(identifier)))
|
|
246
|
-
? undefined
|
|
247
|
-
: yield* snapshot(
|
|
248
|
-
identifier[mapping.identifier.subjectId],
|
|
249
|
-
input.moduleId,
|
|
250
|
-
input.identifier,
|
|
251
|
-
false,
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
const candidate =
|
|
255
|
-
resolved !== undefined &&
|
|
256
|
-
(input.subjectId === undefined ||
|
|
257
|
-
resolved.captured.revision.subjectId === input.subjectId)
|
|
258
|
-
? resolved
|
|
259
|
-
: undefined;
|
|
260
|
-
|
|
261
|
-
const attemptId = yield* allocate(mapping.allocateAttemptId, mapping.allocateAttemptIdSync);
|
|
262
|
-
let used = false;
|
|
263
|
-
|
|
264
|
-
return Object.freeze({
|
|
265
|
-
...(candidate === undefined ? {} : { credential: candidate.captured }),
|
|
266
|
-
admit: <A>(prepare: PreparePasswordCommit<PasswordAttemptAdmission, A>) =>
|
|
267
|
-
Effect.suspend(() => {
|
|
268
|
-
if (used) return Effect.fail(unavailable());
|
|
269
|
-
used = true;
|
|
270
|
-
|
|
271
|
-
return owned(
|
|
272
|
-
Effect.gen(function* () {
|
|
273
|
-
const journal = yield* CurrentCommitJournal;
|
|
274
|
-
const now = yield* nowMillis;
|
|
275
|
-
const captured = candidate?.captured;
|
|
276
|
-
|
|
277
|
-
// This receipt records what was inspected; it grants no authority.
|
|
278
|
-
// Settlement checks the current database against this exact snapshot.
|
|
279
|
-
yield* sqlMapping(
|
|
280
|
-
() =>
|
|
281
|
-
sql`${a.insert(
|
|
282
|
-
mapping.attempt.encodeInsert(
|
|
283
|
-
{
|
|
284
|
-
moduleId: input.moduleId,
|
|
285
|
-
action: input.action,
|
|
286
|
-
attemptId,
|
|
287
|
-
identifier: input.identifier,
|
|
288
|
-
...(captured === undefined
|
|
289
|
-
? {}
|
|
290
|
-
: {
|
|
291
|
-
subjectId: captured.revision.subjectId,
|
|
292
|
-
credentialId: captured.credentialId,
|
|
293
|
-
securityRevision: captured.revision.securityRevision,
|
|
294
|
-
credentialRevision: captured.credentialRevision,
|
|
295
|
-
verifierVersion: captured.verifierVersion,
|
|
296
|
-
identifierBindingRevision: captured.identifierBindingRevision,
|
|
297
|
-
}),
|
|
298
|
-
admittedAtMillis: now,
|
|
299
|
-
deadlineMillis: now + input.attemptLifetimeMillis,
|
|
300
|
-
retentionUntilMillis: now + input.attemptLifetimeMillis,
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
...(candidate === undefined
|
|
304
|
-
? {}
|
|
305
|
-
: { nativeSubjectId: candidate.nativeId }),
|
|
306
|
-
state: "pending",
|
|
307
|
-
},
|
|
308
|
-
),
|
|
309
|
-
)}`,
|
|
310
|
-
).pipe(Effect.flatten);
|
|
311
|
-
|
|
312
|
-
return prepare(
|
|
313
|
-
{
|
|
314
|
-
_tag: "Admitted",
|
|
315
|
-
attemptId,
|
|
316
|
-
...(captured === undefined ? {} : { credential: captured }),
|
|
317
|
-
},
|
|
318
|
-
journal,
|
|
319
|
-
);
|
|
320
|
-
}),
|
|
321
|
-
);
|
|
322
|
-
}),
|
|
323
|
-
});
|
|
324
|
-
}).pipe(failure),
|
|
325
|
-
settleAttempt: (uncaptured, prepare) =>
|
|
326
|
-
owned(
|
|
327
|
-
Effect.gen(function* () {
|
|
328
|
-
const input = Object.freeze({
|
|
329
|
-
...uncaptured,
|
|
330
|
-
...(uncaptured.rehash === undefined
|
|
331
|
-
? {}
|
|
332
|
-
: { rehash: Object.freeze({ ...uncaptured.rehash }) }),
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
const journal = yield* CurrentCommitJournal;
|
|
336
|
-
|
|
337
|
-
const captured =
|
|
338
|
-
input.captured === undefined
|
|
339
|
-
? undefined
|
|
340
|
-
: yield* snapshotPasswordCredential(input.captured);
|
|
341
|
-
|
|
342
|
-
const discovered = yield* attemptRow(input.moduleId, input.attemptId, false);
|
|
343
|
-
|
|
344
|
-
if (discovered === undefined || discovered[mapping.attempt.state] !== "pending")
|
|
345
|
-
return prepare("rejected", journal);
|
|
346
|
-
|
|
347
|
-
// Lock the subject before the attempt: a coordinated application
|
|
348
|
-
// transaction may already hold the subject lock. Without row locks
|
|
349
|
-
// the discovery read is already serialized, so it is not repeated.
|
|
350
|
-
const current =
|
|
351
|
-
input.outcome !== "verified" || captured === undefined
|
|
352
|
-
? undefined
|
|
353
|
-
: yield* snapshot(
|
|
354
|
-
discovered[mapping.attempt.subjectId],
|
|
355
|
-
input.moduleId,
|
|
356
|
-
captured.identifier,
|
|
357
|
-
true,
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
const row = options.locking
|
|
361
|
-
? yield* attemptRow(input.moduleId, input.attemptId, true)
|
|
362
|
-
: discovered;
|
|
363
|
-
|
|
364
|
-
if (row === undefined || row[mapping.attempt.state] !== "pending")
|
|
365
|
-
return prepare("rejected", journal);
|
|
366
|
-
const now = yield* nowMillis;
|
|
367
|
-
const deadline = yield* mapping.decodeInstant(row[mapping.attempt.deadline]);
|
|
368
|
-
|
|
369
|
-
const verified =
|
|
370
|
-
captured !== undefined &&
|
|
371
|
-
current !== undefined &&
|
|
372
|
-
captured.moduleId === input.moduleId &&
|
|
373
|
-
deadline > now &&
|
|
374
|
-
attemptMatches(row, captured, current.nativeId) &&
|
|
375
|
-
samePasswordCredentialSnapshot(current.captured, captured);
|
|
376
|
-
|
|
377
|
-
const decision = verified ? "verified" : "rejected";
|
|
378
|
-
const receipt = prepare(decision, journal);
|
|
379
|
-
|
|
380
|
-
yield* sqlMapping(
|
|
381
|
-
() => sql`${a.update({ [mapping.attempt.state]: decision })}
|
|
382
|
-
where ${a.column(mapping.attempt.moduleId)} = ${a.value(mapping.attempt.moduleId, input.moduleId)}
|
|
383
|
-
and ${a.column(mapping.attempt.attemptId)} = ${a.value(mapping.attempt.attemptId, input.attemptId)}
|
|
384
|
-
and ${a.column(mapping.attempt.state)} = ${a.value(mapping.attempt.state, "pending")}`,
|
|
385
|
-
).pipe(Effect.flatten);
|
|
386
|
-
|
|
387
|
-
if (verified && current !== undefined && captured !== undefined) {
|
|
388
|
-
let expected = current.captured;
|
|
389
|
-
|
|
390
|
-
if (input.rehash !== undefined) {
|
|
391
|
-
if (
|
|
392
|
-
input.rehash.expectedVersion !== expected.verifierVersion ||
|
|
393
|
-
Redacted.value(input.rehash.expectedVerifier) !== Redacted.value(expected.verifier)
|
|
394
|
-
)
|
|
395
|
-
return yield* unavailable();
|
|
396
|
-
|
|
397
|
-
const version = yield* allocate(
|
|
398
|
-
mapping.allocateRevision,
|
|
399
|
-
mapping.allocateRevisionSync,
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
const rehash = input.rehash;
|
|
403
|
-
|
|
404
|
-
yield* sqlMapping(
|
|
405
|
-
() => sql`${c.update(mapping.credential.encodeVerifier(rehash.nextVerifier, version))}
|
|
406
|
-
where ${c.column(mapping.credential.moduleId)} = ${c.value(mapping.credential.moduleId, input.moduleId)}
|
|
407
|
-
and ${c.column(mapping.credential.subjectId)} = ${c.value(mapping.credential.subjectId, current.nativeId)}
|
|
408
|
-
and ${c.column(mapping.credential.credentialId)} = ${c.value(mapping.credential.credentialId, captured.credentialId)}
|
|
409
|
-
and ${c.column(mapping.credential.credentialRevision)} = ${c.value(mapping.credential.credentialRevision, captured.credentialRevision)}
|
|
410
|
-
and ${c.column(mapping.credential.verifierVersion)} = ${c.value(mapping.credential.verifierVersion, rehash.expectedVersion)}
|
|
411
|
-
and ${c.column(mapping.credential.verifier)} = ${c.value(mapping.credential.verifier, Redacted.value(rehash.expectedVerifier))}`,
|
|
412
|
-
).pipe(Effect.flatten);
|
|
413
|
-
expected = {
|
|
414
|
-
...expected,
|
|
415
|
-
verifierVersion: version,
|
|
416
|
-
verifier: input.rehash.nextVerifier,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const settled = expected;
|
|
421
|
-
|
|
422
|
-
const applied = Effect.gen(function* () {
|
|
423
|
-
const final = yield* snapshot(
|
|
424
|
-
current.nativeId,
|
|
425
|
-
input.moduleId,
|
|
426
|
-
captured.identifier,
|
|
427
|
-
false,
|
|
428
|
-
input.attemptId,
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
return (
|
|
432
|
-
final !== undefined &&
|
|
433
|
-
samePasswordCredentialSnapshot(final.captured, settled) &&
|
|
434
|
-
final.attempt !== undefined &&
|
|
435
|
-
final.attempt[mapping.attempt.state] === "verified" &&
|
|
436
|
-
attemptMatches(final.attempt, captured, current.nativeId) &&
|
|
437
|
-
(yield* mapping.decodeInstant(final.attempt[mapping.attempt.deadline])) ===
|
|
438
|
-
deadline &&
|
|
439
|
-
deadline > (yield* nowMillis)
|
|
440
|
-
);
|
|
441
|
-
}).pipe(failure);
|
|
442
|
-
|
|
443
|
-
if (!(yield* applied)) return yield* unavailable();
|
|
444
|
-
|
|
445
|
-
// A coordinator that accepts postconditions repeats this check after
|
|
446
|
-
// application work, inside the same transaction.
|
|
447
|
-
const postconditions = yield* Effect.serviceOption(PasswordPreparedPostconditions);
|
|
448
|
-
|
|
449
|
-
if (
|
|
450
|
-
Option.isSome(postconditions) &&
|
|
451
|
-
!postconditions.value.register(
|
|
452
|
-
Effect.flatMap(applied, (holds) =>
|
|
453
|
-
holds ? Effect.void : Effect.fail(unavailable()),
|
|
454
|
-
),
|
|
455
|
-
)
|
|
456
|
-
)
|
|
457
|
-
return yield* unavailable();
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return receipt;
|
|
461
|
-
}),
|
|
462
|
-
),
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
return service;
|
|
466
|
-
});
|