@yielded/auth-persistence 0.1.0-beta.29 → 0.1.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/Testing.d.mts +44 -0
- package/dist/Testing.mjs +310 -0
- package/dist/Testing.mjs.map +1 -0
- package/dist/internal/models/oauth-model.d.mts +10 -1
- package/dist/internal/models/oauth-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/sql-change.mjs +11 -1
- package/dist/internal/sql-change.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Testing.ts +652 -0
- package/src/internal/models/oauth-model.ts +10 -0
- package/src/internal/oauth/native-accounts.ts +72 -1
- package/src/internal/oauth/native-state.ts +6 -2
- package/src/internal/sql-change.ts +14 -1
|
@@ -8,7 +8,7 @@ import type { Fragment } from "effect/sql/Statement";
|
|
|
8
8
|
import { type OAuthAccountsMapping, OAuthEligibilityFact } from "../models/oauth-model";
|
|
9
9
|
import type { NativeSqlTables } from "../native-sql-table";
|
|
10
10
|
import { makeConditionalSqlInsert } from "../session-native-record";
|
|
11
|
-
import { anySqlCondition, exactSqlText } from "../sql-change";
|
|
11
|
+
import { anySqlCondition, exactSqlText, sqlTextBytes } from "../sql-change";
|
|
12
12
|
import { makeSqlCommitExecutor, SqlBatchCommit, CurrentSqlCommit } from "../sql-commit";
|
|
13
13
|
import type { SqlExpression, TableModel } from "../table-model";
|
|
14
14
|
import { makeOAuthNativeFlow } from "./native-flow";
|
|
@@ -86,6 +86,77 @@ export const makeNativeOAuthAccountsServices = Effect.fnUntraced(function* (
|
|
|
86
86
|
]);
|
|
87
87
|
|
|
88
88
|
const service: M.OAuthAccountsPersistence["Service"] = {
|
|
89
|
+
list: (original) =>
|
|
90
|
+
read(
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
const input = M.snapshotOAuthSync(M.OAuthLinkedAccountsRead, original);
|
|
93
|
+
const nativeId = yield* mapping.subjectId.toNative(input.invocation.subjectId);
|
|
94
|
+
|
|
95
|
+
const metadataAccess = tables.expression(
|
|
96
|
+
mapping.metadataAccess({
|
|
97
|
+
invocation: input.invocation,
|
|
98
|
+
moduleId: input.moduleId,
|
|
99
|
+
subjectId: nativeId,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const s = mapping.subject;
|
|
104
|
+
const orderedId = yield* sqlTextBytes(credential.column(c.credentialId));
|
|
105
|
+
|
|
106
|
+
const cursorCondition =
|
|
107
|
+
input.cursor === undefined
|
|
108
|
+
? sql``
|
|
109
|
+
: sql`and ${orderedId} > ${yield* sqlTextBytes(credential.value(c.credentialId, input.cursor))}`;
|
|
110
|
+
|
|
111
|
+
// Bound candidate work before resolving identities. Each final read uses
|
|
112
|
+
// its columns' own codecs and rechecks live ownership/authority/policy.
|
|
113
|
+
// A removed candidate can leave a short page; the cursor still advances.
|
|
114
|
+
// Match exact credential identity even when the column's collation does not.
|
|
115
|
+
const rows = yield* sql`select ${credential.fields("page_")} from ${credential.name}
|
|
116
|
+
where ${exactSqlText(sql, credential.column(c.moduleId), credential.value(c.moduleId, input.moduleId))}
|
|
117
|
+
and ${credential.column(c.subjectId)} = ${credential.value(c.subjectId, nativeId)}
|
|
118
|
+
and ${tables.expression(c.activeCondition)} and ${metadataAccess}
|
|
119
|
+
and exists(select 1 from ${state.subject.name}
|
|
120
|
+
where ${state.subject.column(s.id)} = ${state.subject.value(s.id, nativeId)}
|
|
121
|
+
and ${tables.expression(s.activeCondition)})
|
|
122
|
+
${cursorCondition}
|
|
123
|
+
order by ${orderedId} limit ${input.limit + 1}`;
|
|
124
|
+
|
|
125
|
+
const selected = rows.slice(0, input.limit);
|
|
126
|
+
const items: Array<M.OAuthLinkedAccount> = [];
|
|
127
|
+
let cursor: string | undefined;
|
|
128
|
+
|
|
129
|
+
for (const row of selected) {
|
|
130
|
+
const credentialId = yield* Schema.decodeUnknownEffect(
|
|
131
|
+
M.OAuthLinkedAccount.fields.credentialId,
|
|
132
|
+
)(credential.decode(row, "page_")[c.credentialId]);
|
|
133
|
+
|
|
134
|
+
cursor = credentialId;
|
|
135
|
+
|
|
136
|
+
const current = yield* state.readCredential(
|
|
137
|
+
{
|
|
138
|
+
moduleId: input.moduleId,
|
|
139
|
+
subjectId: input.invocation.subjectId,
|
|
140
|
+
credentialId,
|
|
141
|
+
},
|
|
142
|
+
metadataAccess,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (current !== undefined)
|
|
146
|
+
items.push(
|
|
147
|
+
M.OAuthLinkedAccount.make({
|
|
148
|
+
credentialId: current.credentialId,
|
|
149
|
+
...current.identity,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return M.snapshotOAuthSync(M.OAuthLinkedAccountsListResult, {
|
|
155
|
+
items,
|
|
156
|
+
...(rows.length > input.limit && cursor !== undefined ? { cursor } : {}),
|
|
157
|
+
});
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
89
160
|
capture: (input) =>
|
|
90
161
|
read(Effect.map(state.capture(input.subjectId, false), (current) => current?.revision)),
|
|
91
162
|
issue: (original, prepare) =>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import type { SubjectId } from "@yielded/auth/Schema";
|
|
8
8
|
import { Effect, Schema } from "effect";
|
|
9
9
|
import { SqlClient } from "effect/sql";
|
|
10
|
+
import type { Fragment } from "effect/sql/Statement";
|
|
10
11
|
|
|
11
12
|
import type { OAuthSignInMapping } from "../models/oauth-model";
|
|
12
13
|
import type { NativeSqlTables } from "../native-sql-table";
|
|
@@ -305,7 +306,10 @@ export const makeOAuthNativeState = Effect.fnUntraced(function* (
|
|
|
305
306
|
return result;
|
|
306
307
|
});
|
|
307
308
|
|
|
308
|
-
const readCredential = Effect.fnUntraced(function* (
|
|
309
|
+
const readCredential = Effect.fnUntraced(function* (
|
|
310
|
+
input: typeof OAuthCredentialKey.Type,
|
|
311
|
+
metadataAccess: Fragment = sql`true`,
|
|
312
|
+
) {
|
|
309
313
|
const nativeId = yield* mapping.subjectId.toNative(input.subjectId);
|
|
310
314
|
let identityKey: unknown;
|
|
311
315
|
|
|
@@ -334,7 +338,7 @@ export const makeOAuthNativeState = Effect.fnUntraced(function* (
|
|
|
334
338
|
and ${owner(o, mapping.ownership.subjectId)}
|
|
335
339
|
join ${active("subject", "oauth_subject")} on ${owner(s, mapping.subject.id)}
|
|
336
340
|
join ${active("authority", "oauth_authority")} on ${owner(a, mapping.authority.subjectId)}
|
|
337
|
-
where ${owner(c, mapping.credential.subjectId)}
|
|
341
|
+
where ${metadataAccess} and ${owner(c, mapping.credential.subjectId)}
|
|
338
342
|
and ${exact(c.column(mapping.credential.moduleId), c.value(mapping.credential.moduleId, input.moduleId))}
|
|
339
343
|
and ${exact(c.column(mapping.credential.credentialId), c.value(mapping.credential.credentialId, input.credentialId))}
|
|
340
344
|
order by ${a.column(mapping.authority.credentialId)} limit 65`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import
|
|
2
|
+
import { SqlClient } from "effect/sql/SqlClient";
|
|
3
3
|
import type { SqlError } from "effect/sql/SqlError";
|
|
4
4
|
import { join, type Fragment } from "effect/sql/Statement";
|
|
5
5
|
|
|
@@ -25,6 +25,19 @@ export const anySqlCondition: (conditions: ReadonlyArray<Fragment>) => Fragment
|
|
|
25
25
|
"1 = 0",
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
+
/** Exact text comparison and ordering, independent of the column's collation. */
|
|
29
|
+
export const sqlTextBytes = Effect.fnUntraced(function* (
|
|
30
|
+
value: Fragment,
|
|
31
|
+
): Effect.fn.Return<Fragment, never, SqlClient> {
|
|
32
|
+
const sql = (yield* SqlClient).withoutTransforms();
|
|
33
|
+
|
|
34
|
+
return sql.onDialectOrElse({
|
|
35
|
+
mysql: () => sql`binary ${value}`,
|
|
36
|
+
pg: () => sql`convert_to(cast(${value} as text), 'UTF8')`,
|
|
37
|
+
orElse: () => sql`cast(${value} as blob)`,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
28
41
|
/** Keep the indexed predicate and reject collation aliases of security tokens. */
|
|
29
42
|
export const exactSqlText = (sql: SqlClient, left: Fragment, right: Fragment) =>
|
|
30
43
|
sql.and([
|