@yielded/auth-persistence 0.1.0-beta.7 → 0.1.0-beta.9
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 +9 -0
- package/dist/DrizzleD1.d.mts +9 -9
- package/dist/DrizzleLibsql.d.mts +25 -25
- package/dist/DrizzleLibsql.mjs +7 -22
- package/dist/DrizzleLibsql.mjs.map +1 -1
- package/dist/DrizzleMysql2.d.mts +15 -15
- package/dist/DrizzlePglite.d.mts +9 -9
- package/dist/DrizzlePostgres.d.mts +15 -15
- package/dist/DrizzleSqliteBun.d.mts +18 -18
- package/dist/DrizzleSqliteDo.d.mts +29 -29
- package/dist/DrizzleSqliteNode.d.mts +24 -24
- package/dist/DrizzleSqliteWasm.d.mts +24 -24
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/drizzle/D1BatchStatements.d.mts +1 -2
- package/dist/drizzle/SuppliedService.d.mts +1 -2
- package/dist/drizzle/d1-emails.d.mts +5 -6
- package/dist/drizzle/d1-identity.d.mts +2 -3
- package/dist/drizzle/d1-oauth.d.mts +10 -11
- package/dist/drizzle/d1-passkey.d.mts +5 -6
- package/dist/drizzle/d1-password-prepared.d.mts +2 -3
- package/dist/drizzle/d1-passwords.d.mts +4 -5
- package/dist/drizzle/d1-phone.d.mts +2 -3
- package/dist/drizzle/d1-planning.d.mts +1 -2
- package/dist/drizzle/d1-proofs.d.mts +2 -3
- package/dist/drizzle/d1-sessions.d.mts +10 -11
- package/dist/drizzle/d1-totp.d.mts +2 -3
- package/dist/drizzle/email-model.d.mts +30 -31
- package/dist/drizzle/email-registration.d.mts +1 -2
- package/dist/drizzle/email-target.d.mts +3 -4
- package/dist/drizzle/model.d.mts +42 -42
- package/dist/drizzle/model.mjs +4 -6
- package/dist/drizzle/model.mjs.map +1 -1
- package/dist/drizzle/mysql-emails.d.mts +7 -8
- package/dist/drizzle/mysql-identity.d.mts +2 -3
- package/dist/drizzle/mysql-password-prepared.d.mts +3 -4
- package/dist/drizzle/mysql-passwords.d.mts +6 -7
- package/dist/drizzle/mysql-proofs.d.mts +3 -4
- package/dist/drizzle/mysql-sessions.d.mts +15 -16
- package/dist/drizzle/mysql.d.mts +9 -10
- package/dist/drizzle/mysql.mjs +1 -1
- package/dist/drizzle/oauth-connected-model.d.mts +21 -22
- package/dist/drizzle/oauth-connected-reference.d.mts +1 -2
- package/dist/drizzle/oauth-connected-state.d.mts +1950 -7
- package/dist/drizzle/oauth-connected-target.d.mts +9 -10
- package/dist/drizzle/oauth-flow.d.mts +22 -2
- package/dist/drizzle/oauth-model.d.mts +45 -46
- package/dist/drizzle/oauth-owner.d.mts +10 -2
- package/dist/drizzle/oauth-target.d.mts +9 -10
- package/dist/drizzle/passkey-model.d.mts +34 -35
- package/dist/drizzle/passkey-registration-ceremony-model.d.mts +5 -6
- package/dist/drizzle/passkey-write-model.d.mts +13 -14
- package/dist/drizzle/password-model.d.mts +29 -30
- package/dist/drizzle/password-prepared-model.d.mts +10 -11
- package/dist/drizzle/password-prepared-target.d.mts +1 -2
- package/dist/drizzle/password-registration.d.mts +1 -2
- package/dist/drizzle/password-target.d.mts +2 -3
- package/dist/drizzle/pg-emails.d.mts +7 -8
- package/dist/drizzle/pg-password-prepared.d.mts +3 -4
- package/dist/drizzle/pg-passwords.d.mts +6 -7
- package/dist/drizzle/pg-proofs.d.mts +3 -4
- package/dist/drizzle/pg-sessions.d.mts +15 -16
- package/dist/drizzle/pg.d.mts +11 -12
- package/dist/drizzle/pg.mjs +1 -1
- package/dist/drizzle/phone-model.d.mts +9 -10
- package/dist/drizzle/phone-target.d.mts +1 -2
- package/dist/drizzle/proof-model.d.mts +25 -26
- package/dist/drizzle/proof-target.d.mts +1 -2
- package/dist/drizzle/session-model.d.mts +25 -26
- package/dist/drizzle/session-target.d.mts +4 -5
- package/dist/drizzle/sqlite.d.mts +9 -10
- package/dist/drizzle/sqlite.mjs +1 -1
- package/dist/drizzle/standalone-guard.mjs +3 -5
- package/dist/drizzle/standalone-guard.mjs.map +1 -1
- package/dist/drizzle/step-up-model.d.mts +6 -7
- package/dist/drizzle/totp-model.d.mts +7 -8
- package/dist/drizzle/totp-state.d.mts +2 -3
- package/dist/drizzle/totp-target.d.mts +1 -2
- package/dist/drizzle/transaction-mysql.mjs +1 -0
- package/dist/drizzle/transaction-mysql.mjs.map +1 -1
- package/dist/drizzle/transaction-owner.d.mts +3 -2
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/dist/internal/configuration.d.mts +19 -19
- package/dist/internal/mapping-error.d.mts +2 -3
- package/dist/internal/oauth-app.d.mts +11 -0
- package/dist/internal/oauth-app.mjs +57 -0
- package/dist/internal/oauth-app.mjs.map +1 -0
- package/dist/internal/oauth-server.d.mts +10 -0
- package/dist/internal/oauth-server.mjs +66 -0
- package/dist/internal/oauth-server.mjs.map +1 -0
- package/dist/internal/passkey/target.d.mts +2 -3
- package/dist/internal/passkeys.mjs +2 -1
- package/dist/internal/passkeys.mjs.map +1 -1
- package/dist/internal/persistence.mjs +3 -2
- package/dist/internal/persistence.mjs.map +1 -1
- package/dist/internal/sql-persistence.d.mts +1 -2
- package/dist/internal/sql-table.d.mts +6 -7
- package/dist/internal/sql-table.mjs +12 -2
- package/dist/internal/sql-table.mjs.map +1 -1
- package/dist/internal/standalone.mjs +13 -0
- package/dist/internal/standalone.mjs.map +1 -0
- package/dist/internal/storage-tables.d.mts +5 -6
- package/dist/internal/transaction-execution-kernel.d.mts +2 -3
- package/dist/internal/transaction-execution-kernel.mjs +3 -5
- package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
- package/dist/internal/transaction-kernel.d.mts +49 -2
- package/package.json +1 -1
- package/src/DrizzleLibsql.ts +10 -54
- package/src/drizzle/model.ts +8 -10
- package/src/drizzle/standalone-guard.ts +4 -9
- package/src/drizzle/transaction-mysql.ts +2 -0
- package/src/index.ts +2 -0
- package/src/internal/oauth-app.ts +82 -0
- package/src/internal/oauth-server.ts +89 -0
- package/src/internal/passkeys.ts +2 -6
- package/src/internal/persistence.ts +3 -5
- package/src/internal/sql-table.ts +17 -2
- package/src/internal/standalone.ts +30 -0
- package/src/internal/transaction-execution-kernel.ts +3 -7
package/src/drizzle/model.ts
CHANGED
|
@@ -22,9 +22,11 @@ import {
|
|
|
22
22
|
type InferSelectModel,
|
|
23
23
|
type Table,
|
|
24
24
|
} from "drizzle-orm";
|
|
25
|
-
import { Context, DateTime, Effect, Layer
|
|
25
|
+
import { Context, DateTime, Effect, Layer } from "effect";
|
|
26
26
|
import type * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
27
27
|
|
|
28
|
+
import { requireStandalone } from "../internal/standalone";
|
|
29
|
+
|
|
28
30
|
export type CommitMode = "interactive" | "synchronous" | "batch";
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -39,16 +41,12 @@ export interface SqlClientTransactionDatabase {
|
|
|
39
41
|
export const requireStandaloneConsume = (
|
|
40
42
|
database: SqlClientTransactionDatabase,
|
|
41
43
|
): Effect.Effect<void, AuthStoreError> =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
onSome: () =>
|
|
47
|
-
AuthStoreError.make({
|
|
48
|
-
message: "Use the decision consume API inside an outer database transaction",
|
|
49
|
-
}),
|
|
44
|
+
requireStandalone(
|
|
45
|
+
() =>
|
|
46
|
+
AuthStoreError.make({
|
|
47
|
+
message: "Use the decision consume API inside an outer database transaction",
|
|
50
48
|
}),
|
|
51
|
-
|
|
49
|
+
database.$client,
|
|
52
50
|
);
|
|
53
51
|
|
|
54
52
|
export type { ChallengeConsumeDecision } from "@yielded/auth/Persistence";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { Context, Effect,
|
|
1
|
+
import { Context, Effect, Predicate } from "effect";
|
|
2
2
|
import type * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
3
3
|
|
|
4
|
+
import { requireStandalone } from "../internal/standalone";
|
|
5
|
+
|
|
4
6
|
type TransactionService = Context.Key<
|
|
5
7
|
SqlClient.TransactionConnection,
|
|
6
8
|
SqlClient.TransactionConnection.Service
|
|
@@ -27,12 +29,5 @@ export const sqlClientStandaloneGuard = <Failure>(
|
|
|
27
29
|
|
|
28
30
|
if (service === undefined) return Effect.suspend(() => Effect.fail(unavailable()));
|
|
29
31
|
|
|
30
|
-
return
|
|
31
|
-
Effect.flatMap(
|
|
32
|
-
Option.match({
|
|
33
|
-
onNone: () => Effect.void,
|
|
34
|
-
onSome: () => Effect.fail(unavailable()),
|
|
35
|
-
}),
|
|
36
|
-
),
|
|
37
|
-
);
|
|
32
|
+
return requireStandalone(unavailable, { transactionService: service });
|
|
38
33
|
};
|
|
@@ -65,6 +65,8 @@ export const mysqlTransaction = <A, E, R, Failure>(
|
|
|
65
65
|
commit: () => statement("COMMIT"),
|
|
66
66
|
rollback: () => statement("ROLLBACK"),
|
|
67
67
|
savepoint: (_connection, id) => statement(`SAVEPOINT effect_auth_oauth_${id}`),
|
|
68
|
+
releaseSavepoint: (_connection, id) =>
|
|
69
|
+
statement(`RELEASE SAVEPOINT effect_auth_oauth_${id}`),
|
|
68
70
|
rollbackSavepoint: (_connection, id) =>
|
|
69
71
|
statement(`ROLLBACK TO SAVEPOINT effect_auth_oauth_${id}`),
|
|
70
72
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { AuthPersistence } from "./internal/sql-persistence";
|
|
2
|
+
export { OAuthAppPersistence } from "./internal/oauth-app";
|
|
3
|
+
export { OAuthServerPersistence } from "./internal/oauth-server";
|
|
2
4
|
export { PersistenceConfigurationError } from "./internal/configuration";
|
|
3
5
|
export { PersistenceMappingError } from "./internal/mapping-error";
|
|
4
6
|
export { StorageColumn, StorageTable } from "./internal/storage-tables";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { OAuthUnavailable } from "@yielded/auth/OAuth";
|
|
2
|
+
import { Persistence, Record } from "@yielded/auth/OAuthApp";
|
|
3
|
+
import { Effect, Layer, Schema } from "effect";
|
|
4
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
5
|
+
|
|
6
|
+
import { requireStandalone } from "./standalone";
|
|
7
|
+
|
|
8
|
+
const codec = Schema.fromJsonString(Record);
|
|
9
|
+
const Row = Schema.Struct({ payload: Schema.String, version: Schema.String });
|
|
10
|
+
|
|
11
|
+
/** Apply with the application's migrations. SQLite (including D1) and PostgreSQL.
|
|
12
|
+
* Each transition is one conditional statement, so D1 needs no interactive
|
|
13
|
+
* transaction and parallel workers share the same refresh/code claim.
|
|
14
|
+
*/
|
|
15
|
+
const migration = `CREATE TABLE yielded_oauth_app (
|
|
16
|
+
namespace TEXT NOT NULL,
|
|
17
|
+
record_key TEXT NOT NULL,
|
|
18
|
+
version TEXT NOT NULL,
|
|
19
|
+
payload TEXT NOT NULL,
|
|
20
|
+
PRIMARY KEY (namespace, record_key)
|
|
21
|
+
)`;
|
|
22
|
+
|
|
23
|
+
const layer = Layer.effect(
|
|
24
|
+
Persistence,
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const sql = yield* SqlClient.SqlClient;
|
|
27
|
+
|
|
28
|
+
if (!sql.onDialectOrElse({ sqlite: () => true, pg: () => true, orElse: () => false }))
|
|
29
|
+
return yield* OAuthUnavailable.make({});
|
|
30
|
+
|
|
31
|
+
const noTransaction = requireStandalone(() => OAuthUnavailable.make({}), sql);
|
|
32
|
+
|
|
33
|
+
const failure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
34
|
+
effect.pipe(Effect.mapError(() => OAuthUnavailable.make({})));
|
|
35
|
+
|
|
36
|
+
return Persistence.of({
|
|
37
|
+
get: Effect.fn("OAuthAppPersistence.get")(function* (namespace, key) {
|
|
38
|
+
yield* noTransaction;
|
|
39
|
+
|
|
40
|
+
const rows = yield* sql<{
|
|
41
|
+
payload: string;
|
|
42
|
+
version: string;
|
|
43
|
+
}>`SELECT payload, version FROM yielded_oauth_app WHERE namespace = ${namespace} AND record_key = ${key}`;
|
|
44
|
+
|
|
45
|
+
if (rows.length === 0) return undefined;
|
|
46
|
+
if (rows.length !== 1) return yield* OAuthUnavailable.make({});
|
|
47
|
+
const row = yield* Schema.decodeEffect(Row)(rows[0]);
|
|
48
|
+
const value = yield* Schema.decodeEffect(codec)(row.payload);
|
|
49
|
+
|
|
50
|
+
if (value.version !== row.version) return yield* OAuthUnavailable.make({});
|
|
51
|
+
|
|
52
|
+
return value;
|
|
53
|
+
}, failure),
|
|
54
|
+
insert: Effect.fn("OAuthAppPersistence.insert")(function* (namespace, key, value) {
|
|
55
|
+
yield* noTransaction;
|
|
56
|
+
const payload = yield* Schema.encodeEffect(codec)(value);
|
|
57
|
+
|
|
58
|
+
const rows =
|
|
59
|
+
yield* sql`INSERT INTO yielded_oauth_app (namespace, record_key, version, payload) VALUES (${namespace}, ${key}, ${value.version}, ${payload}) ON CONFLICT (namespace, record_key) DO NOTHING RETURNING version`;
|
|
60
|
+
|
|
61
|
+
return rows.length === 1;
|
|
62
|
+
}, failure),
|
|
63
|
+
compareAndSet: Effect.fn("OAuthAppPersistence.compareAndSet")(function* (
|
|
64
|
+
namespace,
|
|
65
|
+
key,
|
|
66
|
+
version,
|
|
67
|
+
value,
|
|
68
|
+
) {
|
|
69
|
+
yield* noTransaction;
|
|
70
|
+
if (value.version === version) return yield* OAuthUnavailable.make({});
|
|
71
|
+
const payload = yield* Schema.encodeEffect(codec)(value);
|
|
72
|
+
|
|
73
|
+
const rows =
|
|
74
|
+
yield* sql`UPDATE yielded_oauth_app SET version = ${value.version}, payload = ${payload} WHERE namespace = ${namespace} AND record_key = ${key} AND version = ${version} RETURNING version`;
|
|
75
|
+
|
|
76
|
+
return rows.length === 1;
|
|
77
|
+
}, failure),
|
|
78
|
+
});
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export const OAuthAppPersistence = { layer, migration };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Persistence, Record, Unavailable } from "@yielded/auth/OAuthServer";
|
|
2
|
+
import { Effect, Layer, Schema } from "effect";
|
|
3
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
4
|
+
|
|
5
|
+
import { requireStandalone } from "./standalone";
|
|
6
|
+
|
|
7
|
+
const codec = Schema.fromJsonString(Record);
|
|
8
|
+
|
|
9
|
+
const Row = Schema.Struct({
|
|
10
|
+
payload: Schema.String,
|
|
11
|
+
version: Schema.String,
|
|
12
|
+
revoked: Schema.Literals([0, 1]),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** SQLite (including D1) and PostgreSQL. Apply once through application migrations.
|
|
16
|
+
* Expired records may be deleted when expires_at_millis <= the current time.
|
|
17
|
+
* Revocation has its own monotonic column so it cannot lose a race with rotation.
|
|
18
|
+
*/
|
|
19
|
+
const migration = `CREATE TABLE yielded_oauth_server (
|
|
20
|
+
namespace TEXT NOT NULL,
|
|
21
|
+
grant_id TEXT NOT NULL,
|
|
22
|
+
version TEXT NOT NULL,
|
|
23
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
expires_at_millis BIGINT NOT NULL,
|
|
25
|
+
payload TEXT NOT NULL,
|
|
26
|
+
PRIMARY KEY (namespace, grant_id)
|
|
27
|
+
)`;
|
|
28
|
+
|
|
29
|
+
const layer = Layer.effect(
|
|
30
|
+
Persistence,
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const sql = yield* SqlClient.SqlClient;
|
|
33
|
+
|
|
34
|
+
if (!sql.onDialectOrElse({ sqlite: () => true, pg: () => true, orElse: () => false }))
|
|
35
|
+
return yield* Unavailable.make({});
|
|
36
|
+
const standalone = requireStandalone(() => Unavailable.make({}), sql);
|
|
37
|
+
|
|
38
|
+
const failure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
39
|
+
effect.pipe(Effect.mapError(() => Unavailable.make({})));
|
|
40
|
+
|
|
41
|
+
return Persistence.of({
|
|
42
|
+
get: Effect.fn("OAuthServerPersistence.get")(function* (namespace, id) {
|
|
43
|
+
yield* standalone;
|
|
44
|
+
|
|
45
|
+
const rows =
|
|
46
|
+
yield* sql`SELECT payload, version, revoked FROM yielded_oauth_server WHERE namespace = ${namespace} AND grant_id = ${id}`;
|
|
47
|
+
|
|
48
|
+
if (rows.length === 0) return undefined;
|
|
49
|
+
if (rows.length !== 1) return yield* Unavailable.make({});
|
|
50
|
+
const row = yield* Schema.decodeUnknownEffect(Row)(rows[0]);
|
|
51
|
+
const record = yield* Schema.decodeEffect(codec)(row.payload);
|
|
52
|
+
|
|
53
|
+
if (record.version !== row.version) return yield* Unavailable.make({});
|
|
54
|
+
|
|
55
|
+
return row.revoked === 1 ? { ...record, status: "Revoked" as const } : record;
|
|
56
|
+
}, failure),
|
|
57
|
+
insert: Effect.fn("OAuthServerPersistence.insert")(function* (namespace, id, record) {
|
|
58
|
+
yield* standalone;
|
|
59
|
+
const payload = yield* Schema.encodeEffect(codec)(record);
|
|
60
|
+
|
|
61
|
+
const rows =
|
|
62
|
+
yield* sql`INSERT INTO yielded_oauth_server (namespace, grant_id, version, revoked, expires_at_millis, payload) VALUES (${namespace}, ${id}, ${record.version}, ${record.status === "Revoked" ? 1 : 0}, ${record.expiresAtMillis}, ${payload}) ON CONFLICT (namespace, grant_id) DO NOTHING RETURNING version`;
|
|
63
|
+
|
|
64
|
+
return rows.length === 1;
|
|
65
|
+
}, failure),
|
|
66
|
+
compareAndSet: Effect.fn("OAuthServerPersistence.compareAndSet")(function* (
|
|
67
|
+
namespace,
|
|
68
|
+
id,
|
|
69
|
+
version,
|
|
70
|
+
record,
|
|
71
|
+
) {
|
|
72
|
+
yield* standalone;
|
|
73
|
+
if (version === record.version) return yield* Unavailable.make({});
|
|
74
|
+
const payload = yield* Schema.encodeEffect(codec)(record);
|
|
75
|
+
|
|
76
|
+
const rows =
|
|
77
|
+
yield* sql`UPDATE yielded_oauth_server SET version = ${record.version}, revoked = ${record.status === "Revoked" ? 1 : 0}, expires_at_millis = ${record.expiresAtMillis}, payload = ${payload} WHERE namespace = ${namespace} AND grant_id = ${id} AND version = ${version} AND revoked = 0 RETURNING version`;
|
|
78
|
+
|
|
79
|
+
return rows.length === 1;
|
|
80
|
+
}, failure),
|
|
81
|
+
revoke: Effect.fn("OAuthServerPersistence.revoke")(function* (namespace, id) {
|
|
82
|
+
yield* standalone;
|
|
83
|
+
yield* sql`UPDATE yielded_oauth_server SET revoked = 1 WHERE namespace = ${namespace} AND grant_id = ${id}`;
|
|
84
|
+
}, failure),
|
|
85
|
+
});
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
export const OAuthServerPersistence = { migration, layer };
|
package/src/internal/passkeys.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type { PasskeyTargetConfiguration } from "./passkey/target";
|
|
|
36
36
|
import type { Backend } from "./persistence";
|
|
37
37
|
import type { ProofSqlDatabase } from "./proof-kernel";
|
|
38
38
|
import type { QueryOperations } from "./query-operations";
|
|
39
|
+
import { requireStandalone } from "./standalone";
|
|
39
40
|
import { storageTables, type StorageRole } from "./storage-tables";
|
|
40
41
|
|
|
41
42
|
const profileJson = Schema.fromJsonString(PasskeyProfile);
|
|
@@ -395,12 +396,7 @@ export const makeComposedPasskeys = (
|
|
|
395
396
|
mode: "interactive",
|
|
396
397
|
dialect,
|
|
397
398
|
locking: dialect === "pg",
|
|
398
|
-
standaloneGuard: () =>
|
|
399
|
-
Effect.serviceOption(client.transactionService).pipe(
|
|
400
|
-
Effect.flatMap((value) =>
|
|
401
|
-
value._tag === "None" ? Effect.void : Effect.fail(unavailable()),
|
|
402
|
-
),
|
|
403
|
-
),
|
|
399
|
+
standaloneGuard: () => requireStandalone(unavailable, client),
|
|
404
400
|
};
|
|
405
401
|
|
|
406
402
|
// The backend has already checked the native query builder; schemas check stored values.
|
|
@@ -7,7 +7,7 @@ import { PhoneAdmission, PhoneSignInTargets, PhoneOtpUnavailable } from "@yielde
|
|
|
7
7
|
import { ProofPersistence, ProofUnavailable } from "@yielded/auth/Proofs";
|
|
8
8
|
import { AuthenticationAuthority, SessionUnavailable } from "@yielded/auth/Sessions";
|
|
9
9
|
import type { Table } from "drizzle-orm";
|
|
10
|
-
import { Context, Effect, Layer,
|
|
10
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
11
11
|
import { SqlClient } from "effect/unstable/sql";
|
|
12
12
|
|
|
13
13
|
import {
|
|
@@ -35,6 +35,7 @@ import { makeProofKernel, type ProofSqlDatabase, type ProofSqlQuery } from "./pr
|
|
|
35
35
|
import type { QueryOperations } from "./query-operations";
|
|
36
36
|
import { makeRegistrationAuthority, type CreateSubject } from "./registration";
|
|
37
37
|
import { makeSessionKernel, type SessionSqlDatabase } from "./session-kernel";
|
|
38
|
+
import { requireStandalone } from "./standalone";
|
|
38
39
|
import { makeMappings } from "./storage-mapping";
|
|
39
40
|
import {
|
|
40
41
|
tableDefinition,
|
|
@@ -323,10 +324,7 @@ export const createPersistence = <T extends object, R>(
|
|
|
323
324
|
]);
|
|
324
325
|
const mappings = makeMappings(storage);
|
|
325
326
|
|
|
326
|
-
const standalone = <E>(error: () => E) =>
|
|
327
|
-
Effect.serviceOption(client.transactionService).pipe(
|
|
328
|
-
Effect.flatMap((value) => (Option.isNone(value) ? Effect.void : Effect.fail(error()))),
|
|
329
|
-
);
|
|
327
|
+
const standalone = <E>(error: () => E) => requireStandalone(error, client);
|
|
330
328
|
|
|
331
329
|
// Backend validation owns the foreign query-builder shape, never the decoded rows.
|
|
332
330
|
const native = database as ProofSqlDatabase &
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import { Option, Schema, SchemaGetter } from "effect";
|
|
2
|
+
|
|
1
3
|
import { PersistenceMappingError } from "./mapping-error";
|
|
2
4
|
|
|
5
|
+
const SqlInteger = Schema.Union([
|
|
6
|
+
Schema.Int,
|
|
7
|
+
Schema.BigInt,
|
|
8
|
+
Schema.String.check(Schema.isPattern(/^-?\d+$/)),
|
|
9
|
+
]).pipe(
|
|
10
|
+
Schema.decodeTo(Schema.Int, {
|
|
11
|
+
decode: SchemaGetter.transform(Number),
|
|
12
|
+
encode: SchemaGetter.passthrough(),
|
|
13
|
+
}),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const decodeInteger = Schema.decodeUnknownOption(SqlInteger);
|
|
17
|
+
|
|
3
18
|
export type Dialect = "pg" | "sqlite";
|
|
4
19
|
export type Row = Readonly<Record<string, unknown>>;
|
|
5
20
|
|
|
@@ -52,9 +67,9 @@ export class Column extends Fragment {
|
|
|
52
67
|
throw PersistenceMappingError.make({ operation: "decode", cause: "Invalid SQL boolean" });
|
|
53
68
|
}
|
|
54
69
|
if (options.type === "integer") {
|
|
55
|
-
const number =
|
|
70
|
+
const number = decodeInteger(value);
|
|
56
71
|
|
|
57
|
-
if (
|
|
72
|
+
if (Option.isSome(number)) return number.value;
|
|
58
73
|
throw PersistenceMappingError.make({
|
|
59
74
|
operation: "decode",
|
|
60
75
|
cause: "SQL integer is outside the safe range",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Context, Effect } from "effect";
|
|
2
|
+
import type * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
3
|
+
|
|
4
|
+
/** Standalone operations must observe their own durable commit. */
|
|
5
|
+
export const requireStandalone = <Failure>(
|
|
6
|
+
unavailable: () => Failure,
|
|
7
|
+
client?: {
|
|
8
|
+
readonly transactionService: Context.Key<
|
|
9
|
+
SqlClient.TransactionConnection,
|
|
10
|
+
SqlClient.TransactionConnection.Service
|
|
11
|
+
>;
|
|
12
|
+
},
|
|
13
|
+
): Effect.Effect<void, Failure> =>
|
|
14
|
+
Effect.withFiber((fiber) => {
|
|
15
|
+
const services = fiber.context.mapUnsafe;
|
|
16
|
+
|
|
17
|
+
if (client !== undefined && services.has(client.transactionService.key))
|
|
18
|
+
return Effect.fail(unavailable());
|
|
19
|
+
|
|
20
|
+
// rc.117 libSQL uses a private per-client marker that is not exposed through
|
|
21
|
+
// SqlClient.transactionService. Conservatively reject every ambient libSQL
|
|
22
|
+
// transaction, including another client's, before issuing standalone writes.
|
|
23
|
+
// Recheck this upstream detail whenever the Effect driver is upgraded.
|
|
24
|
+
for (const key of services.keys()) {
|
|
25
|
+
if (key.startsWith("@effect/sql-libsql/LibsqlClient/LibsqlTransaction/"))
|
|
26
|
+
return Effect.fail(unavailable());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Effect.void;
|
|
30
|
+
});
|
|
@@ -7,11 +7,12 @@ import {
|
|
|
7
7
|
} from "@yielded/auth/Hooks";
|
|
8
8
|
/* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */
|
|
9
9
|
/* oxlint-disable no-explicit-any -- private execution bridge retains exact public driver wrappers. */
|
|
10
|
-
import { Context, Effect
|
|
10
|
+
import { Context, Effect } from "effect";
|
|
11
11
|
import type * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
12
12
|
import type * as SqlError from "effect/unstable/sql/SqlError";
|
|
13
13
|
import type { Statement } from "effect/unstable/sql/Statement";
|
|
14
14
|
|
|
15
|
+
import { requireStandalone } from "./standalone";
|
|
15
16
|
import {
|
|
16
17
|
type TransactionNativeDatabase,
|
|
17
18
|
type TransactionOwner,
|
|
@@ -67,12 +68,7 @@ export const makeTransactionExecutionKernel = (
|
|
|
67
68
|
database: {
|
|
68
69
|
readonly $client: Pick<SqlClient.SqlClient, "transactionService">;
|
|
69
70
|
},
|
|
70
|
-
) =>
|
|
71
|
-
Effect.serviceOption(database.$client.transactionService).pipe(
|
|
72
|
-
Effect.flatMap(
|
|
73
|
-
Option.match({ onNone: () => Effect.void, onSome: () => Effect.fail(unavailable()) }),
|
|
74
|
-
),
|
|
75
|
-
);
|
|
71
|
+
) => requireStandalone(unavailable, database.$client);
|
|
76
72
|
|
|
77
73
|
const makeTransactionExecution = <Failure, OwnerId>(
|
|
78
74
|
ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,
|