@yielded/auth-persistence 0.1.0-beta.30 → 0.1.0-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/dist/Adapter.d.mts +1 -1
- package/dist/Adapter.mjs +8 -8
- package/dist/OAuthPersistence.mjs +10 -5
- package/dist/OAuthPersistence.mjs.map +1 -1
- package/dist/internal/configuration.d.mts +49 -13
- package/dist/internal/configuration.mjs.map +1 -1
- package/dist/internal/email-native.mjs +10 -2
- package/dist/internal/email-native.mjs.map +1 -1
- package/dist/internal/email-policy.mjs +3 -2
- package/dist/internal/email-policy.mjs.map +1 -1
- package/dist/internal/models/oauth-model.d.mts +4 -2
- package/dist/internal/models/oauth-model.mjs.map +1 -1
- package/dist/internal/models/password-model.d.mts +2 -1
- package/dist/internal/models/password-model.mjs.map +1 -1
- package/dist/internal/oauth/native-accounts.d.mts +1 -1
- package/dist/internal/oauth/native-accounts.mjs +6 -6
- package/dist/internal/oauth/native-accounts.mjs.map +1 -1
- package/dist/internal/oauth/native-connected.d.mts +1 -1
- package/dist/internal/oauth/native-connected.mjs +1 -1
- package/dist/internal/oauth/native-registration.mjs +19 -8
- package/dist/internal/oauth/native-registration.mjs.map +1 -1
- package/dist/internal/oauth/native-sign-in.d.mts +1 -1
- package/dist/internal/oauth/native-sign-in.mjs +1 -1
- package/dist/internal/oauth/native-state.mjs +21 -11
- package/dist/internal/oauth/native-state.mjs.map +1 -1
- package/dist/internal/oauth/state.d.mts +1 -1
- package/dist/internal/oauth/state.mjs +1 -1
- package/dist/internal/oauth-managed.mjs +182 -0
- package/dist/internal/oauth-managed.mjs.map +1 -0
- package/dist/internal/passkey-managed.mjs +5 -3
- package/dist/internal/passkey-managed.mjs.map +1 -1
- package/dist/internal/passkey-native-registration.mjs +1 -1
- package/dist/internal/passkey-native-state.mjs +33 -23
- package/dist/internal/passkey-native-state.mjs.map +1 -1
- package/dist/internal/passkey-native-write.mjs +3 -3
- package/dist/internal/passkey-native-write.mjs.map +1 -1
- package/dist/internal/passkey-native.d.mts +15 -7
- package/dist/internal/passkey-native.mjs +2 -1
- package/dist/internal/passkey-native.mjs.map +1 -1
- package/dist/internal/passkey-policy.mjs +9 -7
- package/dist/internal/passkey-policy.mjs.map +1 -1
- package/dist/internal/password-native.mjs +22 -5
- package/dist/internal/password-native.mjs.map +1 -1
- package/dist/internal/password-policy.mjs +7 -6
- package/dist/internal/password-policy.mjs.map +1 -1
- package/dist/internal/password-registration-native.d.mts +1 -1
- package/dist/internal/persistence.d.mts +3 -0
- package/dist/internal/persistence.mjs +99 -42
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/phone-native.mjs +1 -1
- package/dist/internal/phone-native.mjs.map +1 -1
- package/dist/internal/registration.mjs +17 -8
- package/dist/internal/registration.mjs.map +1 -1
- package/dist/internal/session-native-authority.mjs +8 -6
- package/dist/internal/session-native-authority.mjs.map +1 -1
- package/dist/internal/session-native-login.d.mts +2 -2
- package/dist/internal/session-native-login.mjs +4 -2
- package/dist/internal/session-native-login.mjs.map +1 -1
- package/dist/internal/session-native-pending-read.mjs +26 -20
- package/dist/internal/session-native-pending-read.mjs.map +1 -1
- package/dist/internal/session-native-record.mjs +6 -4
- package/dist/internal/session-native-record.mjs.map +1 -1
- package/dist/internal/session-native-state.mjs +24 -8
- package/dist/internal/session-native-state.mjs.map +1 -1
- package/dist/internal/session-native-stateful.d.mts +2 -2
- package/dist/internal/session-native-stateful.mjs +67 -38
- package/dist/internal/session-native-stateful.mjs.map +1 -1
- package/dist/internal/session-native-step-up.d.mts +2 -2
- package/dist/internal/session-native-step-up.mjs +17 -19
- package/dist/internal/session-native-step-up.mjs.map +1 -1
- package/dist/internal/session-native-validity.mjs +3 -1
- package/dist/internal/session-native-validity.mjs.map +1 -1
- package/dist/internal/sql-authority.mjs +5 -1
- package/dist/internal/sql-authority.mjs.map +1 -1
- package/dist/internal/sql-storage-validation.mjs +2 -1
- package/dist/internal/sql-storage-validation.mjs.map +1 -1
- package/dist/internal/sql-table.d.mts +1 -0
- package/dist/internal/sql-table.mjs.map +1 -1
- package/dist/internal/sqlite-policy-guard.mjs +23 -0
- package/dist/internal/sqlite-policy-guard.mjs.map +1 -0
- package/dist/internal/step-up-state.mjs +19 -6
- package/dist/internal/step-up-state.mjs.map +1 -1
- package/dist/internal/storage-mapping.d.mts +9 -5
- package/dist/internal/storage-mapping.mjs +17 -5
- package/dist/internal/storage-mapping.mjs.map +1 -1
- package/dist/internal/storage-tables.d.mts +237 -0
- package/dist/internal/storage-tables.mjs +68 -0
- package/dist/internal/storage-tables.mjs.map +1 -1
- package/dist/internal/storage-validation.d.mts +3 -1
- package/dist/internal/storage-validation.mjs +70 -17
- package/dist/internal/storage-validation.mjs.map +1 -1
- package/dist/internal/totp-native.mjs +1 -1
- package/package.json +1 -1
- package/src/OAuthPersistence.ts +7 -2
- package/src/internal/configuration.ts +72 -15
- package/src/internal/email-native.ts +16 -6
- package/src/internal/email-policy.ts +12 -3
- package/src/internal/models/oauth-model.ts +7 -1
- package/src/internal/models/password-model.ts +2 -1
- package/src/internal/oauth/native-accounts.ts +3 -3
- package/src/internal/oauth/native-registration.ts +17 -6
- package/src/internal/oauth/native-state.ts +38 -18
- package/src/internal/oauth-managed.ts +277 -0
- package/src/internal/passkey-managed.ts +8 -2
- package/src/internal/passkey-native-state.ts +59 -52
- package/src/internal/passkey-native-write.ts +2 -2
- package/src/internal/passkey-native.ts +4 -2
- package/src/internal/passkey-policy.ts +13 -9
- package/src/internal/password-native.ts +45 -11
- package/src/internal/password-policy.ts +16 -8
- package/src/internal/persistence.ts +360 -204
- package/src/internal/phone-native.ts +1 -1
- package/src/internal/registration.ts +53 -40
- package/src/internal/session-native-authority.ts +26 -8
- package/src/internal/session-native-login.ts +9 -4
- package/src/internal/session-native-pending-read.ts +46 -33
- package/src/internal/session-native-record.ts +6 -4
- package/src/internal/session-native-state.ts +32 -10
- package/src/internal/session-native-stateful.ts +147 -52
- package/src/internal/session-native-step-up.ts +31 -24
- package/src/internal/session-native-validity.ts +3 -1
- package/src/internal/sql-authority.ts +5 -1
- package/src/internal/sql-storage-validation.ts +53 -46
- package/src/internal/sql-table.ts +1 -0
- package/src/internal/sqlite-policy-guard.ts +34 -0
- package/src/internal/step-up-state.ts +43 -6
- package/src/internal/storage-mapping.ts +40 -4
- package/src/internal/storage-tables.ts +76 -0
- package/src/internal/storage-validation.ts +127 -27
package/src/OAuthPersistence.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
OAuthConnectedRevocations,
|
|
9
9
|
OAuthUnavailable,
|
|
10
10
|
} from "@yielded/auth/OAuth";
|
|
11
|
+
import { reportAuthDiagnostic } from "@yielded/auth/Persistence";
|
|
11
12
|
import { type Context, type Crypto, Effect, Predicate, Schema } from "effect";
|
|
12
13
|
import { SqlClient } from "effect/sql";
|
|
13
14
|
|
|
@@ -49,6 +50,7 @@ import { validateSqlStorage } from "./internal/sql-storage-validation";
|
|
|
49
50
|
import { Table } from "./internal/sql-table";
|
|
50
51
|
import { requireStandalone } from "./internal/standalone";
|
|
51
52
|
import type { StorageTable } from "./internal/storage-tables";
|
|
53
|
+
import { withStorageValidation } from "./internal/storage-validation";
|
|
52
54
|
export * from "./internal/sql-oauth-model";
|
|
53
55
|
export * from "./internal/sql-oauth-connected-model";
|
|
54
56
|
export * from "./internal/oauth/native-layers";
|
|
@@ -77,10 +79,13 @@ const acquire = Effect.gen(function* () {
|
|
|
77
79
|
if (
|
|
78
80
|
!client.onDialectOrElse({ pg: () => true, sqlite: () => true, orElse: () => false }) ||
|
|
79
81
|
Predicate.hasProperty(client, "~@effect/sql-d1/D1Client")
|
|
80
|
-
)
|
|
82
|
+
) {
|
|
83
|
+
yield* reportAuthDiagnostic("persistence-validation", "configuration");
|
|
84
|
+
|
|
81
85
|
return yield* PersistenceConfigurationError.make({
|
|
82
86
|
reason: "OAuth persistence requires PostgreSQL or SQLite with interactive transactions",
|
|
83
87
|
});
|
|
88
|
+
}
|
|
84
89
|
|
|
85
90
|
return { client, tables: makeNativeSqlTables(client) };
|
|
86
91
|
});
|
|
@@ -96,7 +101,7 @@ const using = <M, A, E, R>(
|
|
|
96
101
|
yield* validateSqlStorage(mapping).pipe(Effect.mapError(() => OAuthUnavailable.make({})));
|
|
97
102
|
|
|
98
103
|
return yield* make(target.tables, mapping);
|
|
99
|
-
});
|
|
104
|
+
}).pipe(withStorageValidation);
|
|
100
105
|
|
|
101
106
|
const coordinate = <S, A, E, R, ES, RS>(
|
|
102
107
|
make: Effect.Effect<S, ES, RS>,
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { EmailAction, EmailAddressPersistence } from "@yielded/auth/Email";
|
|
2
2
|
import type { LoginIdentifier } from "@yielded/auth/Identity";
|
|
3
|
+
import type {
|
|
4
|
+
OAuthConnectedPersistence,
|
|
5
|
+
OAuthConnectedPolicy,
|
|
6
|
+
OAuthConnectedRevocations,
|
|
7
|
+
OAuthSignInPersistence,
|
|
8
|
+
} from "@yielded/auth/OAuth";
|
|
3
9
|
import type {
|
|
4
10
|
PasskeyActionChallenge,
|
|
5
11
|
PasskeyConfig,
|
|
@@ -44,10 +50,19 @@ export interface PasskeyFeature {
|
|
|
44
50
|
readonly managementPolicy?: PasskeyManagementPolicy;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
export interface OAuthFeature {
|
|
54
|
+
readonly kind: "oauth";
|
|
55
|
+
readonly moduleId: string;
|
|
56
|
+
readonly signIn: boolean;
|
|
57
|
+
readonly connected?: OAuthConnectedPolicy;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
export interface Strategy {
|
|
48
61
|
readonly strategy: object;
|
|
62
|
+
/** Absent metadata leaves the strategy's persistence services application-supplied. */
|
|
49
63
|
readonly persistence?:
|
|
50
64
|
| PasskeyFeature
|
|
65
|
+
| OAuthFeature
|
|
51
66
|
| {
|
|
52
67
|
readonly kind: "password" | "phone" | "email";
|
|
53
68
|
readonly moduleId: string;
|
|
@@ -69,7 +84,7 @@ export interface Definition<C extends ClaimsCodec, Id extends string> {
|
|
|
69
84
|
type KeyId<T> = T extends Context.Key<infer Id, infer _Service> ? Id : never;
|
|
70
85
|
type Enabled<
|
|
71
86
|
A extends { readonly strategies: Readonly<Record<string, Strategy>> },
|
|
72
|
-
Kind extends "password" | "phone" | "email" | "passkey",
|
|
87
|
+
Kind extends "password" | "phone" | "email" | "passkey" | "oauth",
|
|
73
88
|
> = Extract<A["strategies"][keyof A["strategies"]], { persistence: { kind: Kind } }>;
|
|
74
89
|
|
|
75
90
|
type ManagedPassword<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =
|
|
@@ -82,6 +97,14 @@ export type PasskeyRequirement<
|
|
|
82
97
|
A extends { readonly strategies: Readonly<Record<string, Strategy>> },
|
|
83
98
|
> = Enabled<A, "passkey"> extends never ? never : PasskeyConfig;
|
|
84
99
|
|
|
100
|
+
type OAuthSignIn<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> = Extract<
|
|
101
|
+
Enabled<A, "oauth">,
|
|
102
|
+
{ persistence: { signIn: true } }
|
|
103
|
+
>;
|
|
104
|
+
|
|
105
|
+
type OAuthConnected<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> =
|
|
106
|
+
Extract<Enabled<A, "oauth">, { persistence: { connected: OAuthConnectedPolicy } }>;
|
|
107
|
+
|
|
85
108
|
type RegistrationKey<S> = S extends { readonly RegistrationAuthority: infer K } ? K : never;
|
|
86
109
|
type Registration<S> =
|
|
87
110
|
RegistrationKey<S> extends Context.Key<infer _Id, infer Service>
|
|
@@ -97,6 +120,27 @@ export interface ProvisioningId<Id extends string> {
|
|
|
97
120
|
readonly service: "PersistenceProvisioning";
|
|
98
121
|
}
|
|
99
122
|
|
|
123
|
+
export interface SubjectProvisioningInput<Registration> {
|
|
124
|
+
readonly requestId: string;
|
|
125
|
+
readonly identifier: LoginIdentifier;
|
|
126
|
+
readonly registration: Registration;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Return application subject values for insertion by the auth commit owner.
|
|
130
|
+
* Include the allocated native ID, active status, and initial security revision.
|
|
131
|
+
* Preparing values may allocate IDs, but must not write to the database or perform
|
|
132
|
+
* external side effects. Interactive drivers also accept an existing insert callback.
|
|
133
|
+
*/
|
|
134
|
+
export type SubjectProvisioning<Registration> =
|
|
135
|
+
| {
|
|
136
|
+
readonly values: (
|
|
137
|
+
input: SubjectProvisioningInput<Registration>,
|
|
138
|
+
) => Effect.Effect<Readonly<Record<string, unknown>>, PasswordUnavailable>;
|
|
139
|
+
}
|
|
140
|
+
| ((
|
|
141
|
+
input: SubjectProvisioningInput<Registration>,
|
|
142
|
+
) => Effect.Effect<SubjectId, PasswordUnavailable>);
|
|
143
|
+
|
|
100
144
|
export type Provisioning<A extends { readonly strategies: Readonly<Record<string, Strategy>> }> = {
|
|
101
145
|
readonly [
|
|
102
146
|
K in keyof A["strategies"] as A["strategies"][K] extends {
|
|
@@ -104,11 +148,7 @@ export type Provisioning<A extends { readonly strategies: Readonly<Record<string
|
|
|
104
148
|
}
|
|
105
149
|
? K
|
|
106
150
|
: never
|
|
107
|
-
]:
|
|
108
|
-
readonly requestId: string;
|
|
109
|
-
readonly identifier: LoginIdentifier;
|
|
110
|
-
readonly registration: Registration<A["strategies"][K]>;
|
|
111
|
-
}) => Effect.Effect<SubjectId, PasswordUnavailable>;
|
|
151
|
+
]: SubjectProvisioning<Registration<A["strategies"][K]>>;
|
|
112
152
|
};
|
|
113
153
|
|
|
114
154
|
export type ProvisioningRequirement<
|
|
@@ -124,8 +164,11 @@ type UsesProofs<A extends { readonly strategies: Readonly<Record<string, Strateg
|
|
|
124
164
|
|
|
125
165
|
export type Ports<C extends ClaimsCodec, Id extends string, A extends Definition<C, Id>> =
|
|
126
166
|
| AuthenticationAuthority
|
|
127
|
-
|
|
|
128
|
-
|
|
167
|
+
| ("stateful" extends A["sessionMode"]
|
|
168
|
+
?
|
|
169
|
+
| KeyId<A["sessions"]["StatefulSessionPersistence"]>
|
|
170
|
+
| KeyId<A["sessions"]["SessionRepository"]>
|
|
171
|
+
: never)
|
|
129
172
|
| KeyId<A["sessions"]["PendingAuthentication"]>
|
|
130
173
|
| KeyId<A["sessions"]["SessionStepUpPersistence"]>
|
|
131
174
|
| KeyId<A["sessions"]["SessionCleanup"]>
|
|
@@ -135,17 +178,26 @@ export type Ports<C extends ClaimsCodec, Id extends string, A extends Definition
|
|
|
135
178
|
| (ManagedPasskey<A> extends never ? never : PasskeyManagementPersistence)
|
|
136
179
|
| (Enabled<A, "email"> extends never ? never : EmailAddressPersistence)
|
|
137
180
|
| (UsesProofs<A> extends never ? never : ProofPersistence)
|
|
138
|
-
| (Enabled<A, "phone"> extends never ? never : PhoneSignInTargets)
|
|
181
|
+
| (Enabled<A, "phone"> extends never ? never : PhoneSignInTargets)
|
|
182
|
+
| (OAuthSignIn<A> extends never ? never : OAuthSignInPersistence)
|
|
183
|
+
| (OAuthConnected<A> extends never
|
|
184
|
+
? never
|
|
185
|
+
: OAuthConnectedPersistence | OAuthConnectedRevocations);
|
|
139
186
|
|
|
140
187
|
export type Roles<C extends ClaimsCodec, Id extends string, A extends Definition<C, Id>> =
|
|
141
188
|
| "identifiers"
|
|
142
189
|
| "credentials"
|
|
143
|
-
| "sessions"
|
|
190
|
+
| ("stateful" extends A["sessionMode"] ? "sessions" : never)
|
|
144
191
|
| "pending"
|
|
145
192
|
| (Enabled<A, "password"> extends never ? never : "passwords")
|
|
146
193
|
| (Enabled<A, "email"> extends never ? never : "emailCredentials")
|
|
147
194
|
| (Enabled<A, "passkey"> extends never ? never : "passkeyCredentials" | "passkeyFlows")
|
|
148
|
-
| (UsesProofs<A> extends never ? never : ProofRoles)
|
|
195
|
+
| (UsesProofs<A> extends never ? never : ProofRoles)
|
|
196
|
+
| (Enabled<A, "oauth"> extends never ? never : "oauthIdentities" | "oauthCredentials")
|
|
197
|
+
| (OAuthSignIn<A> extends never ? never : "oauthSignInFlows")
|
|
198
|
+
| (OAuthConnected<A> extends never
|
|
199
|
+
? never
|
|
200
|
+
: "oauthConnectedFlows" | "oauthConnectedGrants" | "oauthConnectedRevocations");
|
|
149
201
|
|
|
150
202
|
export interface SubjectOptions<T extends object, NativeId> {
|
|
151
203
|
readonly table: T;
|
|
@@ -217,14 +269,19 @@ export interface BoundPersistence<
|
|
|
217
269
|
value: StorageLayout<T, Roles<C, Id, A>>,
|
|
218
270
|
) => Layer.Layer<ConfigId<A["namespace"]>>;
|
|
219
271
|
};
|
|
220
|
-
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
272
|
+
/** Supply subject values through `{ values }` on every driver, including D1.
|
|
273
|
+
* Interactive drivers also accept callbacks that insert through the owning
|
|
274
|
+
* SqlClient and return the new subject ID. Never open an independent transaction
|
|
275
|
+
* or perform external side effects while preparing a subject.
|
|
223
276
|
*/
|
|
224
277
|
readonly Provisioning: Context.Service<ProvisioningRequirement<A>, Provisioning<A>>;
|
|
225
|
-
/**
|
|
278
|
+
/** Stateful sessions and OAuth-only stateless sessions are supported. Other session
|
|
279
|
+
* configurations fail acquisition with PersistenceConfigurationError.
|
|
280
|
+
* Capture dependencies at acquisition; validate SQL metadata and configure storage
|
|
226
281
|
* once on the first service operation. Initialization failures become that port's
|
|
227
282
|
* unavailable error. Failure or interruption remains cached until layer reacquisition.
|
|
283
|
+
* Managed OAuth metadata/token-use authorizations bind policyRevision to the subject's
|
|
284
|
+
* securityRevision; changes to application authorization policy must advance it.
|
|
228
285
|
*/
|
|
229
286
|
readonly layer: Layer.Layer<
|
|
230
287
|
Ports<C, Id, A>,
|
|
@@ -250,11 +250,19 @@ export const makeNativeEmailAddressServices = Effect.fnUntraced(function* (
|
|
|
250
250
|
.filter((time) => time > current.now)
|
|
251
251
|
.sort((a, b) => a - b);
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
253
|
+
let deadline: number | undefined;
|
|
254
|
+
|
|
255
|
+
for (const time of boundaries) {
|
|
256
|
+
for (const requirement of requirements) {
|
|
257
|
+
if (
|
|
258
|
+
!(yield* passwordEvidenceSatisfiedAt(input.authorization.evidence, requirement, time))
|
|
259
|
+
) {
|
|
260
|
+
deadline = time;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (deadline !== undefined) break;
|
|
265
|
+
}
|
|
258
266
|
|
|
259
267
|
ensure(deadline !== undefined);
|
|
260
268
|
const freshness = sql`${now} >= ${current.now} and ${now} < ${deadline}`;
|
|
@@ -470,7 +478,9 @@ export const makeNativeEmailAddressServices = Effect.fnUntraced(function* (
|
|
|
470
478
|
);
|
|
471
479
|
const policy = yield* s.decodeActionRequirement(final.subject, action);
|
|
472
480
|
|
|
473
|
-
ensure(
|
|
481
|
+
ensure(
|
|
482
|
+
yield* passwordEvidenceSatisfiedAt(input.authorization.evidence, policy, final.now),
|
|
483
|
+
);
|
|
474
484
|
if (source !== undefined)
|
|
475
485
|
ensure(
|
|
476
486
|
final.sourceIdentifier !== undefined &&
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type AuthenticationRequirement,
|
|
15
15
|
type SecurityRevision,
|
|
16
16
|
} from "@yielded/auth/Sessions";
|
|
17
|
-
import { Effect } from "effect";
|
|
17
|
+
import { DateTime, Effect } from "effect";
|
|
18
18
|
|
|
19
19
|
import { PersistenceMappingError } from "./mapping-error";
|
|
20
20
|
import type { AnyEmailAddressMapping } from "./models/email-model";
|
|
@@ -171,12 +171,21 @@ export const validateEmailMutation = Effect.fn("EmailAddressPersistence.validate
|
|
|
171
171
|
|
|
172
172
|
const currentRequirement = yield* current.requirement;
|
|
173
173
|
|
|
174
|
+
// Address custody retains strict future-proof rejection.
|
|
175
|
+
if (
|
|
176
|
+
input.authorization.evidence.proofs.some(
|
|
177
|
+
(proof) => DateTime.toEpochMillis(proof.verifiedAt) > now,
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return false;
|
|
181
|
+
|
|
174
182
|
return (
|
|
175
|
-
passwordEvidenceSatisfiedAt(
|
|
183
|
+
(yield* passwordEvidenceSatisfiedAt(
|
|
176
184
|
input.authorization.evidence,
|
|
177
185
|
input.authorization.requirement,
|
|
178
186
|
now,
|
|
179
|
-
) &&
|
|
187
|
+
)) &&
|
|
188
|
+
(yield* passwordEvidenceSatisfiedAt(input.authorization.evidence, currentRequirement, now))
|
|
180
189
|
);
|
|
181
190
|
},
|
|
182
191
|
);
|
|
@@ -47,7 +47,11 @@ export interface OAuthSubjectReadTable<
|
|
|
47
47
|
readonly securityRevision: Column<S>;
|
|
48
48
|
readonly isActiveStatus: (value: unknown) => boolean;
|
|
49
49
|
readonly activeCondition: Expression;
|
|
50
|
-
readonly decodeAuthenticationRequirement: (
|
|
50
|
+
readonly decodeAuthenticationRequirement: (
|
|
51
|
+
row: S["select"],
|
|
52
|
+
) =>
|
|
53
|
+
| AuthenticationRequirement
|
|
54
|
+
| Effect.Effect<AuthenticationRequirement, PersistenceMappingError>;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
export interface OAuthSubjectTable<
|
|
@@ -465,6 +469,8 @@ export interface OAuthAccountsMapping<
|
|
|
465
469
|
}
|
|
466
470
|
|
|
467
471
|
export interface OAuthRegistrationAuthority<Registration> {
|
|
472
|
+
/** Only the first confirmed registration supplies its private authentication handoff. */
|
|
473
|
+
readonly authentication?: "first-confirmed-registration";
|
|
468
474
|
readonly read: (
|
|
469
475
|
access: OAuthRegistrationAccess,
|
|
470
476
|
) => Effect.Effect<OAuthRegistrationInspection | undefined, OAuthUnavailable>;
|
|
@@ -23,7 +23,8 @@ export interface PasswordSubjectTable<Subject extends Table, _NativeSubjectId> {
|
|
|
23
23
|
readonly decodeRequirement: (
|
|
24
24
|
row: Subject["select"],
|
|
25
25
|
) => Effect.Effect<AuthenticationRequirement, PersistenceMappingError>;
|
|
26
|
-
/** Current policy for this exact credential mutation
|
|
26
|
+
/** Current policy for this exact credential mutation. Interactive drivers hold
|
|
27
|
+
* the subject lock; D1 guards the physical application columns through commit. */
|
|
27
28
|
readonly decodeActionRequirement: (
|
|
28
29
|
row: Subject["select"],
|
|
29
30
|
action: PasswordAction,
|
|
@@ -14,7 +14,7 @@ import type { SqlExpression, TableModel } from "../table-model";
|
|
|
14
14
|
import { makeOAuthNativeFlow } from "./native-flow";
|
|
15
15
|
import { makeOAuthNativeMutation } from "./native-mutation";
|
|
16
16
|
import { prepareOAuthNative } from "./native-sign-in";
|
|
17
|
-
import { makeOAuthNativeState } from "./native-state";
|
|
17
|
+
import { decodeOAuthRequirement, makeOAuthNativeState } from "./native-state";
|
|
18
18
|
import { invariant, oauthIdentityKey, sameRevision, satisfies, unavailable } from "./state";
|
|
19
19
|
|
|
20
20
|
// Physical metadata is validated by the adapter; schema values remain typed.
|
|
@@ -341,7 +341,7 @@ export const makeNativeOAuthAccountsServices = Effect.fnUntraced(function* (
|
|
|
341
341
|
credentialId,
|
|
342
342
|
credentialRevision,
|
|
343
343
|
revision,
|
|
344
|
-
requirement: mapping.subject
|
|
344
|
+
requirement: yield* decodeOAuthRequirement(mapping.subject, current.subject),
|
|
345
345
|
});
|
|
346
346
|
|
|
347
347
|
return yield* prepareOAuthNative(
|
|
@@ -495,7 +495,7 @@ export const makeNativeOAuthAccountsServices = Effect.fnUntraced(function* (
|
|
|
495
495
|
}
|
|
496
496
|
const methods = [...eligible.values()];
|
|
497
497
|
const facts = methods.map((entry) => entry.fact);
|
|
498
|
-
const requirement = mapping.subject
|
|
498
|
+
const requirement = yield* decodeOAuthRequirement(mapping.subject, current.subject);
|
|
499
499
|
|
|
500
500
|
if (!facts.some((fact) => fact.usablePrimary) || !satisfies(facts, requirement))
|
|
501
501
|
return yield* prepareOAuthNative({ _tag: "LastSignInMethod" }, prepare);
|
|
@@ -293,6 +293,7 @@ export const makeNativeOAuthRegistrationServices = Effect.fnUntraced(function* <
|
|
|
293
293
|
executor.read(Effect.provideService(effect, Crypto.Crypto, crypto));
|
|
294
294
|
|
|
295
295
|
const service: OAuthRegistrationAuthority<Registration> = {
|
|
296
|
+
authentication: "first-confirmed-registration",
|
|
296
297
|
read: (access) =>
|
|
297
298
|
read(
|
|
298
299
|
Effect.map(
|
|
@@ -399,6 +400,7 @@ export const makeNativeOAuthRegistrationServices = Effect.fnUntraced(function* <
|
|
|
399
400
|
(batch === undefined || (yield* sql`select 1 where ${eligible}`).length === 1);
|
|
400
401
|
|
|
401
402
|
let final: Fragment = horizon(expected);
|
|
403
|
+
let authentication: M.OAuthRegistrationAuthentication | undefined;
|
|
402
404
|
|
|
403
405
|
if (admitted) {
|
|
404
406
|
const nativeId = yield* mapping.allocateSubjectId;
|
|
@@ -488,14 +490,20 @@ export const makeNativeOAuthRegistrationServices = Effect.fnUntraced(function* <
|
|
|
488
490
|
|
|
489
491
|
invariant(a.isActiveStatus(factor[a.status]));
|
|
490
492
|
yield* state.change(authority.insert(factor));
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
493
|
+
authentication = M.snapshotOAuthSync(M.OAuthRegistrationAuthentication, {
|
|
494
|
+
intent: expected,
|
|
495
|
+
credentialId,
|
|
496
|
+
credentialRevision: revision,
|
|
497
|
+
revision: {
|
|
495
498
|
subjectId,
|
|
496
499
|
securityRevision: revision,
|
|
497
500
|
credentials: [{ credentialId, revision }],
|
|
498
|
-
}
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
final = sql.and([
|
|
504
|
+
horizon(expected),
|
|
505
|
+
tables.expression(mapping.eligibility.postcondition(policy)),
|
|
506
|
+
mutation.authorityCondition(nativeId, authentication.revision),
|
|
499
507
|
sql`exists(select 1 from ${credential.name} where ${credential.column(c.subjectId)} = ${credential.value(c.subjectId, nativeId)} and ${exactSqlText(sql, credential.column(c.credentialId), credential.value(c.credentialId, credentialId))} and ${exactSqlText(sql, credential.column(c.credentialRevision), credential.value(c.credentialRevision, revision))} and ${exactSqlText(sql, credential.column(c.identityKey), credential.value(c.identityKey, found.identityKey))} and ${tables.expression(c.activeCondition)})`,
|
|
500
508
|
sql`exists(select 1 from ${mutation.ownership.name} where ${mutation.ownerCondition(found.identityKey, nativeId)})`,
|
|
501
509
|
]);
|
|
@@ -520,8 +528,11 @@ export const makeNativeOAuthRegistrationServices = Effect.fnUntraced(function* <
|
|
|
520
528
|
]),
|
|
521
529
|
);
|
|
522
530
|
|
|
531
|
+
if (!admitted) return yield* rejected();
|
|
532
|
+
invariant(authentication !== undefined);
|
|
533
|
+
|
|
523
534
|
return yield* prepareOAuthNative(
|
|
524
|
-
|
|
535
|
+
{ _tag: "Registered", replayed: false, authentication },
|
|
525
536
|
prepare,
|
|
526
537
|
);
|
|
527
538
|
}),
|
|
@@ -5,15 +5,18 @@ import {
|
|
|
5
5
|
type OAuthCredentialKey,
|
|
6
6
|
} from "@yielded/auth/OAuth";
|
|
7
7
|
import type { SubjectId } from "@yielded/auth/Schema";
|
|
8
|
+
import type { AuthenticationRequirement } from "@yielded/auth/Sessions";
|
|
8
9
|
import { Effect, Schema } from "effect";
|
|
9
10
|
import { SqlClient } from "effect/sql";
|
|
10
11
|
import type { Fragment } from "effect/sql/Statement";
|
|
11
12
|
|
|
12
|
-
import type {
|
|
13
|
+
import type { PersistenceMappingError } from "../mapping-error";
|
|
14
|
+
import type { OAuthSignInMapping, OAuthSubjectReadTable } from "../models/oauth-model";
|
|
13
15
|
import type { NativeSqlTables } from "../native-sql-table";
|
|
14
16
|
import { exactSqlText } from "../sql-change";
|
|
17
|
+
import { canJoinTextColumns, withStorageValidation } from "../storage-validation";
|
|
15
18
|
import type { SqlExpression, TableModel } from "../table-model";
|
|
16
|
-
import { invariant, oauthIdentityKey } from "./state";
|
|
19
|
+
import { invariant, oauthIdentityKey, unavailable } from "./state";
|
|
17
20
|
|
|
18
21
|
// Expression handles are checked by the physical compiler, not domain codecs.
|
|
19
22
|
export type OAuthNativeReadMapping = OAuthSignInMapping<
|
|
@@ -28,6 +31,15 @@ export type OAuthNativeReadMapping = OAuthSignInMapping<
|
|
|
28
31
|
|
|
29
32
|
type Row = Readonly<Record<string, unknown>>;
|
|
30
33
|
|
|
34
|
+
export const decodeOAuthRequirement = Effect.fnUntraced(function* (
|
|
35
|
+
subject: OAuthSubjectReadTable<TableModel>,
|
|
36
|
+
row: Row,
|
|
37
|
+
): Effect.fn.Return<AuthenticationRequirement, PersistenceMappingError> {
|
|
38
|
+
const requirement = subject.decodeAuthenticationRequirement(row);
|
|
39
|
+
|
|
40
|
+
return yield* Effect.isEffect(requirement) ? requirement : Effect.succeed(requirement);
|
|
41
|
+
});
|
|
42
|
+
|
|
31
43
|
/** Unlocked snapshots and explicit subject-first reads share decoding rules.
|
|
32
44
|
* No observation registry or discovered-identity placeholder is retained. */
|
|
33
45
|
export const makeOAuthNativeAuthority = Effect.fnUntraced(function* (
|
|
@@ -197,19 +209,31 @@ export const makeOAuthNativeState = Effect.fnUntraced(function* (
|
|
|
197
209
|
return sql`(select * from ${physical.name} where ${tables.expression(descriptor.activeCondition)}) as ${sql(alias)}`;
|
|
198
210
|
};
|
|
199
211
|
|
|
200
|
-
const joinedIds =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
const [joinedIds, joinedIdentityKeys] = yield* Effect.forEach(
|
|
213
|
+
[
|
|
214
|
+
[
|
|
215
|
+
ownership.unencodedTextColumn?.(mapping.ownership.subjectId),
|
|
216
|
+
subject.unencodedTextColumn?.(mapping.subject.id),
|
|
217
|
+
credential.unencodedTextColumn?.(mapping.credential.subjectId),
|
|
218
|
+
authority.unencodedTextColumn?.(mapping.authority.subjectId),
|
|
219
|
+
],
|
|
220
|
+
[
|
|
221
|
+
credential.unencodedTextColumn?.(mapping.credential.identityKey),
|
|
222
|
+
ownership.unencodedTextColumn?.(mapping.ownership.identityKey),
|
|
223
|
+
],
|
|
224
|
+
],
|
|
225
|
+
canJoinTextColumns,
|
|
226
|
+
).pipe(withStorageValidation, Effect.mapError(unavailable));
|
|
205
227
|
|
|
206
228
|
const decodeSnapshot = Effect.fnUntraced(function* (
|
|
207
229
|
rows: ReadonlyArray<Row>,
|
|
208
230
|
moduleId: string,
|
|
209
231
|
nativeId?: unknown,
|
|
210
232
|
) {
|
|
211
|
-
|
|
212
|
-
|
|
233
|
+
const first = rows[0];
|
|
234
|
+
|
|
235
|
+
if (first === undefined) return undefined;
|
|
236
|
+
const owned = o.decode(first, "o_");
|
|
213
237
|
|
|
214
238
|
const identity = yield* Schema.decodeUnknownEffect(OAuthExternalIdentity)({
|
|
215
239
|
provider: owned[mapping.ownership.provider],
|
|
@@ -223,14 +247,14 @@ export const makeOAuthNativeState = Effect.fnUntraced(function* (
|
|
|
223
247
|
const ownerId = mapping.ownership.decodeSubjectId(owned);
|
|
224
248
|
|
|
225
249
|
if (nativeId !== undefined) invariant(mapping.subjectId.equals(ownerId, nativeId));
|
|
226
|
-
const login = c.decode(
|
|
250
|
+
const login = c.decode(first, "c_");
|
|
227
251
|
|
|
228
252
|
invariant(mapping.subjectId.equals(login[mapping.credential.subjectId], ownerId));
|
|
229
253
|
invariant(login[mapping.credential.identityKey] === identityKey);
|
|
230
254
|
invariant(mapping.credential.isActiveStatus(login[mapping.credential.status]));
|
|
231
255
|
|
|
232
256
|
const current = yield* revision(
|
|
233
|
-
s.decode(
|
|
257
|
+
s.decode(first, "s_"),
|
|
234
258
|
rows.map((row) => a.decode(row, "a_")),
|
|
235
259
|
ownerId,
|
|
236
260
|
);
|
|
@@ -251,7 +275,7 @@ export const makeOAuthNativeState = Effect.fnUntraced(function* (
|
|
|
251
275
|
credentialId,
|
|
252
276
|
credentialRevision,
|
|
253
277
|
revision: current,
|
|
254
|
-
requirement: mapping.subject
|
|
278
|
+
requirement: yield* decodeOAuthRequirement(mapping.subject, s.decode(first, "s_")),
|
|
255
279
|
});
|
|
256
280
|
});
|
|
257
281
|
|
|
@@ -313,11 +337,7 @@ export const makeOAuthNativeState = Effect.fnUntraced(function* (
|
|
|
313
337
|
const nativeId = yield* mapping.subjectId.toNative(input.subjectId);
|
|
314
338
|
let identityKey: unknown;
|
|
315
339
|
|
|
316
|
-
|
|
317
|
-
credential.unencodedTextColumn?.(mapping.credential.identityKey) !== undefined &&
|
|
318
|
-
ownership.unencodedTextColumn?.(mapping.ownership.identityKey) !== undefined;
|
|
319
|
-
|
|
320
|
-
if (!direct) {
|
|
340
|
+
if (!joinedIdentityKeys) {
|
|
321
341
|
const selected = yield* sql`select ${credential.fields("c_")} from ${credential.name}
|
|
322
342
|
where ${exact(credential.column(mapping.credential.moduleId), credential.value(mapping.credential.moduleId, input.moduleId))}
|
|
323
343
|
and ${exact(credential.column(mapping.credential.credentialId), credential.value(mapping.credential.credentialId, input.credentialId))}
|
|
@@ -334,7 +354,7 @@ export const makeOAuthNativeState = Effect.fnUntraced(function* (
|
|
|
334
354
|
const rows =
|
|
335
355
|
yield* sql`select ${o.fields("o_")}, ${s.fields("s_")}, ${c.fields("c_")}, ${a.fields("a_")}
|
|
336
356
|
from ${active("credential", "oauth_credential")}
|
|
337
|
-
join ${o.name} on ${
|
|
357
|
+
join ${o.name} on ${joinedIdentityKeys ? exact(o.column(mapping.ownership.identityKey), c.column(mapping.credential.identityKey)) : exact(o.column(mapping.ownership.identityKey), o.value(mapping.ownership.identityKey, identityKey))}
|
|
338
358
|
and ${owner(o, mapping.ownership.subjectId)}
|
|
339
359
|
join ${active("subject", "oauth_subject")} on ${owner(s, mapping.subject.id)}
|
|
340
360
|
join ${active("authority", "oauth_authority")} on ${owner(a, mapping.authority.subjectId)}
|