@yielded/auth-persistence 0.1.0-beta.30 → 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 +3 -6
- package/dist/Adapter.d.mts +1 -1
- package/dist/internal/configuration.d.mts +18 -8
- package/dist/internal/configuration.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/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/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/internal/configuration.ts +26 -8
- package/src/internal/models/password-model.ts +2 -1
- 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/sqlite-policy-guard.ts +34 -0
- package/src/internal/storage-mapping.ts +40 -4
|
@@ -65,6 +65,8 @@ export interface Backend<T extends object, R, Database extends object = object>
|
|
|
65
65
|
readonly describe: (table: T) => StorageTable;
|
|
66
66
|
readonly acquire: Effect.Effect<Database, PersistenceConfigurationError, R | SqlClient.SqlClient>;
|
|
67
67
|
readonly nativeTables: (database: Database) => NativeSqlTables;
|
|
68
|
+
/** Fixed atomic batches replace interactive transactions on backends such as D1. */
|
|
69
|
+
readonly batch?: (database: Database) => NonNullable<SqlBatchCommit["Service"]>;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const configError = (reason: string) => PersistenceConfigurationError.make({ reason });
|
|
@@ -233,194 +235,202 @@ export const createPersistence = <T extends object, R, Database extends object =
|
|
|
233
235
|
};
|
|
234
236
|
|
|
235
237
|
const services = Layer.effectContext(
|
|
236
|
-
Effect.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
238
|
+
Effect.flatMap(backend.acquire, (database) =>
|
|
239
|
+
Effect.gen(function* () {
|
|
240
|
+
const storage = yield* ConfigKey;
|
|
241
|
+
const client = yield* SqlClient.SqlClient;
|
|
242
|
+
|
|
243
|
+
const dialect = client.onDialectOrElse({
|
|
244
|
+
pg: () => "pg" as const,
|
|
245
|
+
sqlite: () => "sqlite" as const,
|
|
246
|
+
orElse: () => undefined,
|
|
247
|
+
});
|
|
246
248
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
if (dialect === undefined)
|
|
250
|
+
return yield* configError("Use an explicit adapter for this SQL dialect");
|
|
251
|
+
if (auth.sessionMode !== "stateful")
|
|
252
|
+
return yield* configError("The composed layer currently requires stateful sessions");
|
|
253
|
+
if (
|
|
254
|
+
features.some(
|
|
255
|
+
(feature) =>
|
|
256
|
+
feature === undefined ||
|
|
257
|
+
("lifecycle" in feature && feature.lifecycle) ||
|
|
258
|
+
(feature.kind === "email" && !feature.addresses),
|
|
259
|
+
)
|
|
257
260
|
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const validations: StorageValidation[] = [];
|
|
261
|
+
return yield* configError(
|
|
262
|
+
"Use explicit services for strategies without composed persistence support",
|
|
263
|
+
);
|
|
264
|
+
if (storage.namespace !== auth.namespace)
|
|
265
|
+
return yield* configError(
|
|
266
|
+
"Persistence configuration belongs to a different Auth definition",
|
|
267
|
+
);
|
|
268
|
+
const validations: StorageValidation[] = [];
|
|
267
269
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
for (const role of roles) {
|
|
271
|
+
const table = (storage.schema as Partial<Record<StorageRole, T>>)[role];
|
|
270
272
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
if (table === undefined) return yield* configError(`Missing ${role} table`);
|
|
274
|
+
const description = backend.describe(table);
|
|
275
|
+
const columns = description.columns;
|
|
274
276
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
for (const name of Object.keys(storageTables[role].columns)) {
|
|
278
|
+
if (columns[name] === undefined)
|
|
279
|
+
return yield* configError(`Missing ${role}.${name} column`);
|
|
280
|
+
}
|
|
281
|
+
validations.push({ table: description, required: storageTables[role].unique });
|
|
278
282
|
}
|
|
279
|
-
validations.push({
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
nativeTables,
|
|
294
|
-
sessionMapping,
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
const { authenticationAuthority } = yield* makeNativeAuthenticationAuthorityServices(
|
|
298
|
-
nativeTables,
|
|
299
|
-
sessionMapping,
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
const { pendingAuthentication } = yield* makeNativePendingAuthenticationServices(
|
|
303
|
-
nativeTables,
|
|
304
|
-
pendingMapping,
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
const { sessionStepUpPersistence } = yield* makeNativeSessionStepUpServices(nativeTables, {
|
|
308
|
-
...pendingMapping,
|
|
309
|
-
source: {
|
|
310
|
-
kind: "Stateful",
|
|
311
|
-
session: sessionMapping.session,
|
|
312
|
-
sessionId: sessionMapping.sessionId,
|
|
313
|
-
constraints: { sessionDigest: "unique(session.digest)" },
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
const { sessionCleanup } = yield* makeNativeSessionCleanupServices(
|
|
318
|
-
nativeTables,
|
|
319
|
-
pendingMapping,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
let context: Context.Context<never> = Context.make(
|
|
323
|
-
AuthenticationAuthority,
|
|
324
|
-
authenticationAuthority,
|
|
325
|
-
).pipe(
|
|
326
|
-
Context.add(
|
|
327
|
-
auth.sessions.StatefulSessionPersistence,
|
|
328
|
-
sessionServices.statefulSessionPersistence,
|
|
329
|
-
),
|
|
330
|
-
Context.add(auth.sessions.SessionRepository, sessionServices.sessionRepository),
|
|
331
|
-
Context.add(auth.sessions.PendingAuthentication, pendingAuthentication),
|
|
332
|
-
Context.add(auth.sessions.SessionStepUpPersistence, sessionStepUpPersistence),
|
|
333
|
-
Context.add(auth.sessions.SessionCleanup, sessionCleanup),
|
|
334
|
-
);
|
|
283
|
+
validations.push({
|
|
284
|
+
table: backend.describe(storage.subjects.table as T),
|
|
285
|
+
required: [[storage.subjects.id]],
|
|
286
|
+
});
|
|
287
|
+
yield* validateStorageBatch(dialect, validations);
|
|
288
|
+
const nativeTables = backend.nativeTables(database);
|
|
289
|
+
const mappings = yield* makeMappings(storage, nativeTables);
|
|
290
|
+
const sessionMapping = mappings.sessions(auth.claims, auth.sessions.moduleId);
|
|
291
|
+
const pendingMapping = mappings.pending(auth.claims, auth.sessions.moduleId);
|
|
292
|
+
|
|
293
|
+
const sessionServices = yield* makeNativeStatefulSessionServices(
|
|
294
|
+
nativeTables,
|
|
295
|
+
sessionMapping,
|
|
296
|
+
);
|
|
335
297
|
|
|
336
|
-
|
|
298
|
+
const { authenticationAuthority } = yield* makeNativeAuthenticationAuthorityServices(
|
|
299
|
+
nativeTables,
|
|
300
|
+
sessionMapping,
|
|
301
|
+
);
|
|
337
302
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
proofMapping,
|
|
303
|
+
const { pendingAuthentication } = yield* makeNativePendingAuthenticationServices(
|
|
304
|
+
nativeTables,
|
|
305
|
+
pendingMapping,
|
|
342
306
|
);
|
|
343
307
|
|
|
344
|
-
|
|
345
|
-
|
|
308
|
+
const { sessionStepUpPersistence } = yield* makeNativeSessionStepUpServices(
|
|
309
|
+
nativeTables,
|
|
310
|
+
{
|
|
311
|
+
...pendingMapping,
|
|
312
|
+
source: {
|
|
313
|
+
kind: "Stateful",
|
|
314
|
+
session: sessionMapping.session,
|
|
315
|
+
sessionId: sessionMapping.sessionId,
|
|
316
|
+
constraints: { sessionDigest: "unique(session.digest)" },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
);
|
|
346
320
|
|
|
347
|
-
|
|
348
|
-
|
|
321
|
+
const { sessionCleanup } = yield* makeNativeSessionCleanupServices(
|
|
322
|
+
nativeTables,
|
|
323
|
+
pendingMapping,
|
|
324
|
+
);
|
|
349
325
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
326
|
+
let context: Context.Context<never> = Context.make(
|
|
327
|
+
AuthenticationAuthority,
|
|
328
|
+
authenticationAuthority,
|
|
329
|
+
).pipe(
|
|
330
|
+
Context.add(
|
|
331
|
+
auth.sessions.StatefulSessionPersistence,
|
|
332
|
+
sessionServices.statefulSessionPersistence,
|
|
333
|
+
),
|
|
334
|
+
Context.add(auth.sessions.SessionRepository, sessionServices.sessionRepository),
|
|
335
|
+
Context.add(auth.sessions.PendingAuthentication, pendingAuthentication),
|
|
336
|
+
Context.add(auth.sessions.SessionStepUpPersistence, sessionStepUpPersistence),
|
|
337
|
+
Context.add(auth.sessions.SessionCleanup, sessionCleanup),
|
|
354
338
|
);
|
|
355
339
|
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
for (const [name, strategy] of Object.entries(auth.strategies)) {
|
|
359
|
-
if (strategy.persistence?.kind !== "password" || !strategy.persistence.management)
|
|
360
|
-
continue;
|
|
361
|
-
const key = strategy.RegistrationAuthority;
|
|
340
|
+
const proofMapping = proofs ? mappings.proofs() : undefined;
|
|
362
341
|
|
|
363
|
-
if (
|
|
364
|
-
|
|
342
|
+
if (proofMapping !== undefined) {
|
|
343
|
+
const services = yield* makeNativeProofServices(
|
|
344
|
+
backend.nativeTables(database),
|
|
345
|
+
proofMapping,
|
|
346
|
+
);
|
|
365
347
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
mappings.passwords(),
|
|
369
|
-
ProvisioningKey,
|
|
370
|
-
name,
|
|
371
|
-
);
|
|
348
|
+
context = Context.add(context, ProofPersistence, services.proofPersistence);
|
|
349
|
+
}
|
|
372
350
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
context,
|
|
378
|
-
EmailAddressPersistence,
|
|
379
|
-
(yield* makeNativeEmailAddressServices(
|
|
351
|
+
if (password) {
|
|
352
|
+
const mapping = mappings.passwords();
|
|
353
|
+
|
|
354
|
+
const services = yield* makeNativePasswordServices(
|
|
380
355
|
backend.nativeTables(database),
|
|
381
|
-
|
|
356
|
+
mapping,
|
|
382
357
|
proofMapping,
|
|
383
|
-
)
|
|
384
|
-
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
context = Context.add(context, PasswordPersistence, services.passwordPersistence);
|
|
361
|
+
}
|
|
362
|
+
for (const [name, strategy] of Object.entries(auth.strategies)) {
|
|
363
|
+
if (strategy.persistence?.kind !== "password" || !strategy.persistence.management)
|
|
364
|
+
continue;
|
|
365
|
+
const key = strategy.RegistrationAuthority;
|
|
385
366
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
yield* makeComposedPhoneTargets(
|
|
367
|
+
if (key === undefined)
|
|
368
|
+
return yield* configError(`Missing subject provisioning for ${name}`);
|
|
369
|
+
|
|
370
|
+
const registration = yield* makeRegistrationAuthority(
|
|
391
371
|
backend.nativeTables(database),
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
)
|
|
396
|
-
);
|
|
372
|
+
mappings.passwords(),
|
|
373
|
+
ProvisioningKey,
|
|
374
|
+
name,
|
|
375
|
+
);
|
|
397
376
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
377
|
+
context = Context.add(context, key, registration);
|
|
378
|
+
}
|
|
379
|
+
if (email)
|
|
380
|
+
context = Context.add(
|
|
381
|
+
context,
|
|
382
|
+
EmailAddressPersistence,
|
|
383
|
+
(yield* makeNativeEmailAddressServices(
|
|
384
|
+
backend.nativeTables(database),
|
|
385
|
+
mappings.emails(),
|
|
386
|
+
proofMapping,
|
|
387
|
+
)).emailAddressPersistence,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
if (phone)
|
|
391
|
+
context = Context.add(
|
|
392
|
+
context,
|
|
393
|
+
PhoneSignInTargets,
|
|
394
|
+
yield* makeComposedPhoneTargets(
|
|
395
|
+
backend.nativeTables(database),
|
|
396
|
+
storage,
|
|
397
|
+
mappings.proofs(),
|
|
398
|
+
features.flatMap((feature) =>
|
|
399
|
+
feature?.kind === "phone" ? [feature.moduleId] : [],
|
|
400
|
+
),
|
|
408
401
|
),
|
|
409
|
-
|
|
410
|
-
backend.nativeTables(database),
|
|
411
|
-
) as Effect.Effect<
|
|
412
|
-
Context.Context<never>,
|
|
413
|
-
PersistenceConfigurationError,
|
|
414
|
-
PasskeyRequirement<A> | Crypto.Crypto | LifecycleHooks | SqlClient.SqlClient
|
|
415
|
-
>;
|
|
416
|
-
|
|
417
|
-
context = Context.merge(context, yield* services);
|
|
418
|
-
}
|
|
402
|
+
);
|
|
419
403
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
404
|
+
if (passkeys.length > 0) {
|
|
405
|
+
// Auth capability metadata determines whether PasskeyConfig is required.
|
|
406
|
+
const services = makeManagedPasskeys(
|
|
407
|
+
{
|
|
408
|
+
storage,
|
|
409
|
+
namespace: auth.namespace,
|
|
410
|
+
dialect,
|
|
411
|
+
features: passkeys,
|
|
412
|
+
passwordModules: features.flatMap((feature) =>
|
|
413
|
+
feature?.kind === "password" ? [feature.moduleId] : [],
|
|
414
|
+
),
|
|
415
|
+
},
|
|
416
|
+
backend.nativeTables(database),
|
|
417
|
+
) as Effect.Effect<
|
|
418
|
+
Context.Context<never>,
|
|
419
|
+
PersistenceConfigurationError,
|
|
420
|
+
| PasskeyRequirement<A>
|
|
421
|
+
| Crypto.Crypto
|
|
422
|
+
| LifecycleHooks
|
|
423
|
+
| SqlClient.SqlClient
|
|
424
|
+
| SqlBatchCommit
|
|
425
|
+
>;
|
|
426
|
+
|
|
427
|
+
context = Context.merge(context, yield* services);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// The checked capability metadata above determines exactly these service keys.
|
|
431
|
+
return context as Context.Context<Ports<C, Id, A>>;
|
|
432
|
+
}).pipe(Effect.provideService(SqlBatchCommit, backend.batch?.(database))),
|
|
433
|
+
).pipe(
|
|
424
434
|
withStorageValidation,
|
|
425
435
|
Effect.mapError((error) =>
|
|
426
436
|
Schema.is(PersistenceConfigurationError)(error)
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { LifecycleHooks } from "@yielded/auth/Hooks";
|
|
2
|
-
import type { LoginIdentifier } from "@yielded/auth/Identity";
|
|
3
2
|
import { PasswordUnavailable } from "@yielded/auth/Password";
|
|
4
|
-
import type { SubjectId } from "@yielded/auth/Schema";
|
|
5
3
|
import { SecurityRevision } from "@yielded/auth/Sessions";
|
|
6
4
|
import { type Context, Crypto, Effect, Schema, Option, Cause } from "effect";
|
|
7
5
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
8
6
|
|
|
9
|
-
import { PersistenceConfigurationError } from "./configuration";
|
|
7
|
+
import { PersistenceConfigurationError, type SubjectProvisioning } from "./configuration";
|
|
10
8
|
import { randomId } from "./crypto";
|
|
11
9
|
import type { AnyPasswordPersistenceMapping } from "./models/password-model";
|
|
12
10
|
import type { NativeSqlTables } from "./native-sql-table";
|
|
@@ -15,13 +13,9 @@ import {
|
|
|
15
13
|
PasswordIdentifierTaken,
|
|
16
14
|
} from "./password-registration-native";
|
|
17
15
|
import type { PasswordRegistrationAuthority } from "./registration-contract";
|
|
18
|
-
import { makeSqlCommitExecutor, CurrentSqlCommit } from "./sql-commit";
|
|
16
|
+
import { makeSqlCommitExecutor, CurrentSqlCommit, SqlBatchCommit } from "./sql-commit";
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
readonly requestId: string;
|
|
22
|
-
readonly identifier: LoginIdentifier;
|
|
23
|
-
readonly registration: unknown;
|
|
24
|
-
}) => Effect.Effect<SubjectId, PasswordUnavailable>;
|
|
18
|
+
const SubjectValues = Schema.Record(Schema.String, Schema.Unknown);
|
|
25
19
|
|
|
26
20
|
/** Application provisioning and the unverified identifier/password binding share
|
|
27
21
|
* one physical owner. Identifier uniqueness arbitrates registration; correlation
|
|
@@ -34,49 +28,56 @@ export const makeRegistrationAuthority = Effect.fnUntraced(function* <R>(
|
|
|
34
28
|
): Effect.fn.Return<
|
|
35
29
|
PasswordRegistrationAuthority<unknown>,
|
|
36
30
|
PersistenceConfigurationError,
|
|
37
|
-
LifecycleHooks | SqlClient | Crypto.Crypto | R
|
|
31
|
+
LifecycleHooks | SqlClient | Crypto.Crypto | SqlBatchCommit | R
|
|
38
32
|
> {
|
|
39
33
|
const sql = (yield* SqlClient).withoutTransforms();
|
|
40
34
|
const crypto = yield* Crypto.Crypto;
|
|
41
35
|
const executor = yield* makeSqlCommitExecutor(() => PasswordUnavailable.make({}));
|
|
42
36
|
const parent = yield* Effect.serviceOption(CurrentSqlCommit);
|
|
43
|
-
const
|
|
37
|
+
const batch = yield* SqlBatchCommit;
|
|
38
|
+
const store = yield* makeNativePasswordRegistrationStore(tables, mapping, batch !== undefined);
|
|
44
39
|
// The strategy has already decoded its registration Schema. Only its callback
|
|
45
40
|
// signature is erased by the heterogeneous strategy service map.
|
|
46
|
-
const creators = (yield* provisioning) as Readonly<Record<string,
|
|
41
|
+
const creators = (yield* provisioning) as Readonly<Record<string, SubjectProvisioning<unknown>>>;
|
|
47
42
|
const createSubject = creators[strategy];
|
|
48
43
|
|
|
49
44
|
if (createSubject === undefined)
|
|
50
45
|
return yield* PersistenceConfigurationError.make({
|
|
51
46
|
reason: `Missing subject provisioning for ${strategy}`,
|
|
52
47
|
});
|
|
48
|
+
if (batch !== undefined && typeof createSubject === "function")
|
|
49
|
+
return yield* PersistenceConfigurationError.make({
|
|
50
|
+
reason: `D1 subject provisioning for ${strategy} requires { values }; return the new subject row instead of inserting it`,
|
|
51
|
+
});
|
|
53
52
|
const s = mapping.subject;
|
|
54
53
|
const subject = tables(s.table);
|
|
55
54
|
const lock = sql.onDialectOrElse({ sqlite: () => sql``, orElse: () => sql`for update` });
|
|
56
55
|
|
|
57
56
|
const run = <A, E, R2>(work: Effect.Effect<A, E, R2>, suppressed: Effect.Effect<A, E, R2>) =>
|
|
58
|
-
|
|
59
|
-
? executor.
|
|
60
|
-
:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
batch !== undefined
|
|
58
|
+
? executor.batch(work).pipe(Effect.provideService(SqlBatchCommit, batch))
|
|
59
|
+
: Option.isSome(parent)
|
|
60
|
+
? executor.run(work)
|
|
61
|
+
: executor
|
|
62
|
+
.operation(
|
|
63
|
+
work.pipe(
|
|
64
|
+
Effect.mapError((error) =>
|
|
65
|
+
error instanceof PasswordIdentifierTaken ? error : PasswordUnavailable.make({}),
|
|
66
|
+
),
|
|
65
67
|
),
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
68
|
+
)
|
|
69
|
+
.pipe(
|
|
70
|
+
Effect.catchCause((cause) =>
|
|
71
|
+
cause.reasons.length > 0 &&
|
|
72
|
+
cause.reasons.every(
|
|
73
|
+
(reason) =>
|
|
74
|
+
Cause.isFailReason(reason) && reason.error instanceof PasswordIdentifierTaken,
|
|
75
|
+
)
|
|
76
|
+
? executor.run(suppressed, "statement")
|
|
77
|
+
: Effect.failCause(cause),
|
|
78
|
+
),
|
|
79
|
+
Effect.mapError(() => PasswordUnavailable.make({})),
|
|
80
|
+
);
|
|
80
81
|
|
|
81
82
|
return {
|
|
82
83
|
register: (input, project) =>
|
|
@@ -86,26 +87,38 @@ export const makeRegistrationAuthority = Effect.fnUntraced(function* <R>(
|
|
|
86
87
|
? executor.run(store.prepare({ _tag: "Suppressed" }, project), "statement")
|
|
87
88
|
: run(
|
|
88
89
|
Effect.gen(function* () {
|
|
89
|
-
const
|
|
90
|
+
const provisioningInput = {
|
|
90
91
|
requestId: input.requestId,
|
|
91
92
|
identifier: input.identifier,
|
|
92
93
|
registration: input.registration,
|
|
93
|
-
}
|
|
94
|
+
};
|
|
94
95
|
|
|
95
|
-
const
|
|
96
|
+
const row = yield* Effect.gen(function* () {
|
|
97
|
+
if (typeof createSubject !== "function")
|
|
98
|
+
return yield* createSubject
|
|
99
|
+
.values(provisioningInput)
|
|
100
|
+
.pipe(Effect.flatMap(Schema.decodeEffect(SubjectValues)));
|
|
101
|
+
const subjectId = yield* createSubject(provisioningInput);
|
|
102
|
+
const nativeId = yield* mapping.subjectId.toNative(subjectId);
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
const rows =
|
|
105
|
+
yield* sql`select ${subject.fields("registration_subject_")} from ${subject.name} where ${subject.column(s.id)} = ${subject.value(s.id, nativeId)} limit 2 ${lock}`;
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
if (rows.length !== 1) return yield* PasswordUnavailable.make({});
|
|
108
|
+
|
|
109
|
+
return subject.decode(rows[0]!, "registration_subject_");
|
|
110
|
+
});
|
|
102
111
|
|
|
103
112
|
if (!s.isActiveStatus(row[s.status])) return yield* PasswordUnavailable.make({});
|
|
113
|
+
const subjectId = yield* mapping.subjectId.toSubject(row[s.id]);
|
|
104
114
|
|
|
105
115
|
const securityRevision = yield* Schema.decodeUnknownEffect(SecurityRevision)(
|
|
106
116
|
row[s.securityRevision],
|
|
107
117
|
);
|
|
108
118
|
|
|
119
|
+
if (typeof createSubject !== "function")
|
|
120
|
+
yield* store.stage(sql`${subject.insert(row)}`);
|
|
121
|
+
|
|
109
122
|
const identifierRevision = SecurityRevision.make(yield* randomId);
|
|
110
123
|
const credentialId = yield* randomId;
|
|
111
124
|
const credentialRevision = SecurityRevision.make(yield* randomId);
|
|
@@ -195,7 +195,9 @@ export const makeNativeAuthenticationAuthorityServices = Effect.fnUntraced(funct
|
|
|
195
195
|
: yield* consume(input.pending, input.evidence, current.native);
|
|
196
196
|
|
|
197
197
|
const deadline = Math.min(assessment.validUntil, expires, pendingExpiry ?? Infinity);
|
|
198
|
-
|
|
198
|
+
|
|
199
|
+
yield* state.guardPolicy(current);
|
|
200
|
+
const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${deadline}`;
|
|
199
201
|
|
|
200
202
|
if (batch !== undefined) {
|
|
201
203
|
yield* appendSqlBatchStatement(sqlBatchAssertion(sql, condition));
|
|
@@ -129,7 +129,9 @@ export const makeNativePendingAuthenticationServices = Effect.fnUntraced(functio
|
|
|
129
129
|
|
|
130
130
|
const record: PendingAuthenticationRecord<Claims> = { ...input, version };
|
|
131
131
|
const snapshot = yield* mapping.login.encode(record);
|
|
132
|
-
|
|
132
|
+
|
|
133
|
+
yield* state.guardPolicy(current);
|
|
134
|
+
const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${expires}`;
|
|
133
135
|
|
|
134
136
|
if (batch !== undefined)
|
|
135
137
|
yield* appendSqlBatchStatement(sqlBatchAssertion(sql, condition));
|
|
@@ -47,7 +47,7 @@ export const makeNativeSessionPendingReader = Effect.fnUntraced(function* (
|
|
|
47
47
|
const authority = yield* state.decode(
|
|
48
48
|
record.subjectId,
|
|
49
49
|
owner,
|
|
50
|
-
|
|
50
|
+
rows[0],
|
|
51
51
|
rows.map((row) => ct.decode(row, "c_")),
|
|
52
52
|
rows[0].engine_now,
|
|
53
53
|
);
|