@yielded/auth-persistence 0.1.0-beta.17 → 0.1.0-beta.19

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.
Files changed (85) hide show
  1. package/README.md +5 -2
  2. package/dist/Adapter.d.mts +2 -2
  3. package/dist/Adapter.mjs +2 -2
  4. package/dist/internal/configuration.d.mts +5 -4
  5. package/dist/internal/configuration.mjs.map +1 -1
  6. package/dist/internal/crypto.mjs +12 -0
  7. package/dist/internal/crypto.mjs.map +1 -0
  8. package/dist/internal/email-kernel.d.mts +7 -7
  9. package/dist/internal/passkey/admission.mjs +19 -15
  10. package/dist/internal/passkey/admission.mjs.map +1 -1
  11. package/dist/internal/passkey/credentials.mjs +4 -4
  12. package/dist/internal/passkey/credentials.mjs.map +1 -1
  13. package/dist/internal/passkey/enrollment.mjs +6 -6
  14. package/dist/internal/passkey/enrollment.mjs.map +1 -1
  15. package/dist/internal/passkey/flow.mjs +7 -7
  16. package/dist/internal/passkey/flow.mjs.map +1 -1
  17. package/dist/internal/passkey/management.mjs +2 -2
  18. package/dist/internal/passkey/management.mjs.map +1 -1
  19. package/dist/internal/passkey/registration-ceremony.mjs +2 -2
  20. package/dist/internal/passkey/registration-ceremony.mjs.map +1 -1
  21. package/dist/internal/passkey/registration-custody.mjs +1 -1
  22. package/dist/internal/passkey/registration-custody.mjs.map +1 -1
  23. package/dist/internal/passkey/registration-write.mjs +12 -12
  24. package/dist/internal/passkey/registration-write.mjs.map +1 -1
  25. package/dist/internal/passkey/state.d.mts +1 -1
  26. package/dist/internal/passkey/state.mjs +8 -9
  27. package/dist/internal/passkey/state.mjs.map +1 -1
  28. package/dist/internal/passkey/target.d.mts +1 -1
  29. package/dist/internal/passkey/target.mjs.map +1 -1
  30. package/dist/internal/passkey/write-state.mjs +13 -13
  31. package/dist/internal/passkey/write-state.mjs.map +1 -1
  32. package/dist/internal/passkey/write-target.mjs.map +1 -1
  33. package/dist/internal/passkey-kernel.d.mts +705 -249
  34. package/dist/internal/passkeys.mjs +32 -14
  35. package/dist/internal/passkeys.mjs.map +1 -1
  36. package/dist/internal/password-kernel.d.mts +6 -6
  37. package/dist/internal/password-kernel.mjs +1 -1
  38. package/dist/internal/password-kernel.mjs.map +1 -1
  39. package/dist/internal/persistence.d.mts +2 -2
  40. package/dist/internal/persistence.mjs +18 -9
  41. package/dist/internal/persistence.mjs.map +1 -1
  42. package/dist/internal/phone-kernel.d.mts +7 -7
  43. package/dist/internal/phone-kernel.mjs +14 -14
  44. package/dist/internal/phone-kernel.mjs.map +1 -1
  45. package/dist/internal/registration.mjs +8 -7
  46. package/dist/internal/registration.mjs.map +1 -1
  47. package/dist/internal/session-kernel.mjs +2 -1
  48. package/dist/internal/session-kernel.mjs.map +1 -1
  49. package/dist/internal/sql-query.mjs +1 -1
  50. package/dist/internal/sql-query.mjs.map +1 -1
  51. package/dist/internal/storage-mapping.d.mts +5 -4
  52. package/dist/internal/storage-mapping.mjs +16 -13
  53. package/dist/internal/storage-mapping.mjs.map +1 -1
  54. package/dist/internal/storage-validation.d.mts +13 -3
  55. package/dist/internal/storage-validation.mjs +128 -68
  56. package/dist/internal/storage-validation.mjs.map +1 -1
  57. package/dist/internal/transaction-execution-kernel.d.mts +4 -4
  58. package/dist/internal/transaction-execution-kernel.mjs +7 -5
  59. package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
  60. package/package.json +1 -1
  61. package/src/Adapter.ts +7 -1
  62. package/src/internal/configuration.ts +5 -3
  63. package/src/internal/crypto.ts +15 -0
  64. package/src/internal/passkey/admission.ts +22 -15
  65. package/src/internal/passkey/credentials.ts +4 -4
  66. package/src/internal/passkey/enrollment.ts +7 -7
  67. package/src/internal/passkey/flow.ts +8 -8
  68. package/src/internal/passkey/management.ts +7 -2
  69. package/src/internal/passkey/registration-ceremony.ts +2 -2
  70. package/src/internal/passkey/registration-custody.ts +1 -1
  71. package/src/internal/passkey/registration-write.ts +12 -12
  72. package/src/internal/passkey/state.ts +10 -13
  73. package/src/internal/passkey/target.ts +16 -8
  74. package/src/internal/passkey/write-state.ts +10 -14
  75. package/src/internal/passkey/write-target.ts +11 -3
  76. package/src/internal/passkeys.ts +121 -70
  77. package/src/internal/password-kernel.ts +2 -2
  78. package/src/internal/persistence.ts +30 -13
  79. package/src/internal/phone-kernel.ts +26 -17
  80. package/src/internal/registration.ts +10 -8
  81. package/src/internal/session-kernel.ts +3 -2
  82. package/src/internal/sql-query.ts +3 -2
  83. package/src/internal/storage-mapping.ts +21 -13
  84. package/src/internal/storage-validation.ts +219 -139
  85. package/src/internal/transaction-execution-kernel.ts +30 -11
@@ -1,85 +1,145 @@
1
1
  import { PersistenceConfigurationError } from "./configuration.mjs";
2
- import { Effect, Schema } from "effect";
2
+ import { Context, Effect, Schema } from "effect";
3
3
  import { SqlClient } from "effect/sql/SqlClient";
4
4
  //#region src/internal/storage-validation.ts
5
- const Column = Schema.Struct({
6
- name: Schema.String,
7
- pk: Schema.Int
5
+ const Columns = Schema.Array(Schema.String);
6
+ const Keys = Schema.Array(Columns);
7
+ const Metadata = Schema.Struct({
8
+ relation: Schema.String,
9
+ columns: Columns,
10
+ keys: Keys
8
11
  });
9
- const Index = Schema.Struct({
12
+ const PgMetadata = Schema.Array(Metadata);
13
+ const SqliteMetadata = Schema.Array(Schema.Struct({
14
+ relation: Schema.String,
15
+ columns: Schema.fromJsonString(Columns),
16
+ keys: Schema.fromJsonString(Keys)
17
+ }));
18
+ const MysqlColumns = Schema.Array(Schema.Struct({
19
+ relation: Schema.String,
20
+ name: Schema.String
21
+ }));
22
+ const MysqlIndexes = Schema.Array(Schema.Struct({
23
+ relation: Schema.String,
10
24
  name: Schema.String,
11
- unique: Schema.Int,
12
- partial: Schema.Int
13
- });
14
- const IndexColumn = Schema.Struct({ name: Schema.NullOr(Schema.String) });
15
- const PgColumns = Schema.Array(Schema.Struct({ name: Schema.String }));
16
- const PgKeys = Schema.Array(Schema.Struct({ columns: Schema.Array(Schema.String) }));
25
+ columnName: Schema.NullOr(Schema.String),
26
+ prefix: Schema.NullOr(Schema.Number)
27
+ }));
17
28
  const quote = (value) => "\"" + value.replaceAll("\"", "\"\"") + "\"";
18
29
  const qualifiedTableName = (table) => (table.schema === void 0 ? "" : quote(table.schema) + ".") + quote(table.name);
19
- /** Validate physical uniqueness before the first authentication operation. A
20
- * partial or expression index cannot establish these unconditional guarantees. */
21
- const validateStorage = Effect.fnUntraced(function* (dialect, table, required) {
30
+ const validateKeys = Effect.fnUntraced(function* (table, required, physicalKeys) {
31
+ for (const keys of required) {
32
+ const columns = keys.map((key) => table.columns[key]?.name);
33
+ if (columns.some((column) => column === void 0) || !physicalKeys.some((actual) => actual.length > 0 && actual.length <= columns.length && actual.every((column) => columns.includes(column)))) return yield* PersistenceConfigurationError.make({ reason: `Missing unique key on ${table.name} (${keys.join(", ")})` });
34
+ }
35
+ });
36
+ const CurrentMetadata = Context.Reference("@yielded/auth-persistence/StorageMetadata", { defaultValue: () => void 0 });
37
+ /** Share metadata only while constructing one service graph. Captured contexts
38
+ * cannot reuse it after acquisition succeeds, fails, or is interrupted. */
39
+ const withStorageValidation = Effect.fnUntraced(function* (effect) {
40
+ if ((yield* CurrentMetadata)?.active) return yield* effect;
41
+ const cache = {
42
+ active: true,
43
+ clients: /* @__PURE__ */ new Map()
44
+ };
45
+ return yield* effect.pipe(Effect.provideService(CurrentMetadata, cache), Effect.ensuring(Effect.sync(() => {
46
+ cache.active = false;
47
+ cache.clients.clear();
48
+ })));
49
+ });
50
+ /** Batch physical metadata reads, reusing only the current acquisition snapshot.
51
+ * Every requirement is still checked; later acquisitions read fresh metadata. */
52
+ const validateStorageBatch = Effect.fnUntraced(function* (dialect, requirements) {
53
+ if (requirements.length === 0) return;
54
+ if (dialect === "sqlite" && requirements.some(({ table }) => table.schema !== void 0 && table.schema !== "main")) return yield* PersistenceConfigurationError.make({ reason: "SQLite storage must belong to the main database" });
22
55
  const client = yield* SqlClient;
23
- const physicalKeys = yield* dialect === "sqlite" ? Effect.gen(function* () {
24
- if (table.schema !== void 0 && table.schema !== "main") return yield* PersistenceConfigurationError.make({ reason: "SQLite storage must belong to the main database" });
25
- const columns = yield* client`select name, pk from pragma_table_info(${table.name})`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Column))));
26
- for (const column of Object.values(table.columns)) if (!columns.some((actual) => actual.name === column.name)) return yield* PersistenceConfigurationError.make({ reason: `Missing SQL column ${table.name}.${column.name}` });
27
- const keys = [];
28
- const primary = columns.filter((column) => column.pk > 0).sort((a, b) => a.pk - b.pk);
29
- if (primary.length > 0) keys.push(primary.map((column) => column.name));
30
- const indexes = yield* client`select name, "unique", partial from pragma_index_list(${table.name})`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Index))));
31
- for (const index of indexes) {
32
- if (index.unique !== 1 || index.partial !== 0) continue;
33
- const columns = yield* client`select name from pragma_index_info(${index.name})`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(IndexColumn))));
34
- if (columns.every((column) => column.name !== null)) keys.push(columns.flatMap((column) => column.name === null ? [] : [column.name]));
35
- }
36
- return keys;
37
- }) : dialect === "mysql" ? Effect.gen(function* () {
38
- const columns = yield* client`
39
- select column_name as name from information_schema.columns
40
- where table_schema = coalesce(${table.schema ?? null}, database())
41
- and table_name = ${table.name}
42
- `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgColumns)));
43
- for (const column of Object.values(table.columns)) if (!columns.some((actual) => actual.name === column.name)) return yield* PersistenceConfigurationError.make({ reason: `Missing SQL column ${table.name}.${column.name}` });
44
- const indexes = yield* client`
45
- select index_name as name, column_name as columnName, sub_part as prefix
46
- from information_schema.statistics
47
- where table_schema = coalesce(${table.schema ?? null}, database())
48
- and table_name = ${table.name} and non_unique = 0
49
- order by index_name, seq_in_index
50
- `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({
51
- name: Schema.String,
52
- columnName: Schema.NullOr(Schema.String),
53
- prefix: Schema.NullOr(Schema.Number)
54
- })))));
55
- const keys = /* @__PURE__ */ new Map();
56
- for (const index of indexes) {
57
- const columns = keys.get(index.name) ?? [];
58
- columns.push(index);
59
- keys.set(index.name, columns);
60
- }
61
- return [...keys.values()].filter((columns) => columns.every((column) => column.columnName !== null && column.prefix === null)).map((columns) => columns.flatMap((column) => column.columnName === null ? [] : [column.columnName]));
62
- }) : Effect.gen(function* () {
63
- const relation = qualifiedTableName(table);
64
- const columns = yield* client`select attname as name from pg_attribute where attrelid = to_regclass(${relation}) and attnum > 0 and not attisdropped`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgColumns)));
65
- for (const column of Object.values(table.columns)) if (!columns.some((actual) => actual.name === column.name)) return yield* PersistenceConfigurationError.make({ reason: `Missing SQL column ${relation}.${column.name}` });
66
- return (yield* client`
67
- select array_agg(a.attname order by k.ordinality) as columns
56
+ const cache = yield* CurrentMetadata;
57
+ const byRelation = cache?.active ? cache.clients.get(client) ?? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map();
58
+ if (cache?.active) cache.clients.set(client, byRelation);
59
+ const missing = [...new Map(requirements.map(({ table }) => [qualifiedTableName(table), table])).entries()].filter(([relation]) => !byRelation.has(`${dialect}:${relation}`));
60
+ const relations = missing.map(([relation]) => relation);
61
+ if (relations.length > 0) {
62
+ const metadata = yield* dialect === "sqlite" ? Effect.gen(function* () {
63
+ const requested = client.join(", ", false)(missing.map(([relation, table]) => client`(${relation}, ${table.name})`));
64
+ return yield* client`
65
+ with requested(relation, name) as (values ${requested})
66
+ select requested.relation,
67
+ (select json_group_array(name) from pragma_table_info(requested.name)) as columns,
68
+ (select json_group_array(json(storage_key.columns)) from (
69
+ select json_group_array(name) as columns from (
70
+ select name from pragma_table_info(requested.name) where pk > 0 order by pk
71
+ )
72
+ union all
73
+ select (select json_group_array(name) from pragma_index_info(i.name)) as columns
74
+ from pragma_index_list(requested.name) i
75
+ where i."unique" = 1 and i.partial = 0
76
+ and not exists(select 1 from pragma_index_info(i.name) where name is null)
77
+ ) storage_key where json_array_length(storage_key.columns) > 0) as keys
78
+ from requested
79
+ `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(SqliteMetadata)));
80
+ }) : dialect === "mysql" ? Effect.gen(function* () {
81
+ const requested = client.join(" union all ", false)(missing.map(([relation, table]) => client`select ${relation} as relation, ${table.schema ?? null} as schemaName, ${table.name} as tableName`));
82
+ const columns = yield* client`
83
+ select requested.relation, c.column_name as name
84
+ from (${requested}) requested join information_schema.columns c
85
+ on c.table_schema = coalesce(requested.schemaName, database()) and c.table_name = requested.tableName
86
+ `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(MysqlColumns)));
87
+ const indexes = yield* client`
88
+ select requested.relation, i.index_name as name, i.column_name as columnName, i.sub_part as prefix
89
+ from (${requested}) requested join information_schema.statistics i
90
+ on i.table_schema = coalesce(requested.schemaName, database()) and i.table_name = requested.tableName
91
+ where i.non_unique = 0 order by requested.relation, i.index_name, i.seq_in_index
92
+ `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(MysqlIndexes)));
93
+ return relations.map((relation) => {
94
+ const grouped = /* @__PURE__ */ new Map();
95
+ for (const index of indexes) {
96
+ if (index.relation !== relation) continue;
97
+ const entries = grouped.get(index.name) ?? [];
98
+ entries.push(index);
99
+ grouped.set(index.name, entries);
100
+ }
101
+ return {
102
+ relation,
103
+ columns: columns.filter((column) => column.relation === relation).map((column) => column.name),
104
+ keys: [...grouped.values()].filter((entries) => entries.every((entry) => entry.columnName !== null && entry.prefix === null)).map((entries) => entries.flatMap((entry) => entry.columnName === null ? [] : [entry.columnName]))
105
+ };
106
+ });
107
+ }) : Effect.gen(function* () {
108
+ const requested = client.join(", ", false)(relations.map((relation) => client`(${relation}::text)`));
109
+ return yield* client`
110
+ select requested.relation,
111
+ array(select a.attname::text from pg_attribute a
112
+ where a.attrelid = to_regclass(requested.relation)
113
+ and a.attnum > 0 and not a.attisdropped) as columns,
114
+ coalesce((select jsonb_agg(storage_key.columns) from (
115
+ select array_agg(a.attname::text order by k.ordinality) as columns
68
116
  from pg_index i
69
117
  cross join lateral unnest(i.indkey) with ordinality as k(attnum, ordinality)
70
118
  join pg_attribute a on a.attrelid = i.indrelid and a.attnum = k.attnum
71
- where i.indrelid = to_regclass(${relation}) and i.indisunique and i.indisvalid
72
- and i.indimmediate and i.indpred is null and i.indexprs is null
119
+ where i.indrelid = to_regclass(requested.relation)
120
+ and i.indisunique and i.indisvalid and i.indimmediate
121
+ and i.indpred is null and i.indexprs is null
73
122
  and k.ordinality <= i.indnkeyatts
74
123
  group by i.indexrelid
75
- `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgKeys)))).map((index) => index.columns);
76
- });
77
- for (const keys of required) {
78
- const columns = keys.map((key) => table.columns[key]?.name);
79
- if (columns.some((column) => column === void 0) || !physicalKeys.some((actual) => actual.length > 0 && actual.length <= columns.length && actual.every((column) => columns.includes(column)))) return yield* PersistenceConfigurationError.make({ reason: `Missing unique key on ${table.name} (${keys.join(", ")})` });
124
+ ) storage_key), '[]'::jsonb) as keys
125
+ from (values ${requested}) requested(relation)
126
+ `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgMetadata)));
127
+ });
128
+ for (const row of metadata) byRelation.set(`${dialect}:${row.relation}`, row);
129
+ }
130
+ for (const { table, required } of requirements) {
131
+ const relation = qualifiedTableName(table);
132
+ const actual = byRelation.get(`${dialect}:${relation}`);
133
+ for (const column of Object.values(table.columns)) if (actual === void 0 || !actual.columns.includes(column.name)) return yield* PersistenceConfigurationError.make({ reason: `Missing SQL column ${relation}.${column.name}` });
134
+ yield* validateKeys(table, required, actual?.keys ?? []);
80
135
  }
81
- }, (effect, _dialect, table) => effect.pipe(Effect.mapError((error) => Schema.is(PersistenceConfigurationError)(error) ? error : PersistenceConfigurationError.make({ reason: `Cannot validate SQL storage for ${table.name}` }))));
136
+ }, Effect.mapError((error) => Schema.is(PersistenceConfigurationError)(error) ? error : PersistenceConfigurationError.make({ reason: "Cannot validate SQL storage metadata" })));
137
+ /** Validate physical columns and unconditional uniqueness before operations run. */
138
+ const validateStorage = (dialect, table, required) => validateStorageBatch(dialect, [{
139
+ table,
140
+ required
141
+ }]);
82
142
  //#endregion
83
- export { qualifiedTableName, validateStorage };
143
+ export { qualifiedTableName, validateStorage, validateStorageBatch, withStorageValidation };
84
144
 
85
145
  //# sourceMappingURL=storage-validation.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-validation.mjs","names":[],"sources":["../../src/internal/storage-validation.ts"],"sourcesContent":["import { Effect, Schema } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\n\nimport { PersistenceConfigurationError } from \"./configuration\";\nimport type { StorageTable } from \"./storage-tables\";\n\nconst Column = Schema.Struct({ name: Schema.String, pk: Schema.Int });\nconst Index = Schema.Struct({ name: Schema.String, unique: Schema.Int, partial: Schema.Int });\nconst IndexColumn = Schema.Struct({ name: Schema.NullOr(Schema.String) });\nconst PgColumns = Schema.Array(Schema.Struct({ name: Schema.String }));\nconst PgKeys = Schema.Array(Schema.Struct({ columns: Schema.Array(Schema.String) }));\nconst quote = (value: string) => '\"' + value.replaceAll('\"', '\"\"') + '\"';\n\n/** Physical metadata needed for catalog validation; value codecs remain adapter-owned. */\nexport interface PhysicalStorageTable {\n readonly name: string;\n readonly schema?: string;\n readonly columns: Readonly<Record<string, { readonly name: string }>>;\n}\n\nexport const qualifiedTableName = (table: Pick<StorageTable, \"name\" | \"schema\">) =>\n (table.schema === undefined ? \"\" : quote(table.schema) + \".\") + quote(table.name);\n\n/** Validate physical uniqueness before the first authentication operation. A\n * partial or expression index cannot establish these unconditional guarantees. */\nexport const validateStorage = Effect.fnUntraced(\n function* (\n dialect: \"pg\" | \"mysql\" | \"sqlite\",\n table: PhysicalStorageTable,\n required: ReadonlyArray<ReadonlyArray<string>>,\n ) {\n const client = yield* SqlClient;\n\n const physicalKeys: ReadonlyArray<ReadonlyArray<string>> = yield* dialect === \"sqlite\"\n ? Effect.gen(function* () {\n if (table.schema !== undefined && table.schema !== \"main\")\n return yield* PersistenceConfigurationError.make({\n reason: \"SQLite storage must belong to the main database\",\n });\n\n const columns = yield* client`select name, pk from pragma_table_info(${table.name})`.pipe(\n Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Column))),\n );\n\n for (const column of Object.values(table.columns)) {\n if (!columns.some((actual) => actual.name === column.name))\n return yield* PersistenceConfigurationError.make({\n reason: `Missing SQL column ${table.name}.${column.name}`,\n });\n }\n const keys: string[][] = [];\n const primary = columns.filter((column) => column.pk > 0).sort((a, b) => a.pk - b.pk);\n\n if (primary.length > 0) keys.push(primary.map((column) => column.name));\n\n const indexes =\n yield* client`select name, \"unique\", partial from pragma_index_list(${table.name})`.pipe(\n Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Index))),\n );\n\n for (const index of indexes) {\n if (index.unique !== 1 || index.partial !== 0) continue;\n\n const columns = yield* client`select name from pragma_index_info(${index.name})`.pipe(\n Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(IndexColumn))),\n );\n\n if (columns.every((column) => column.name !== null))\n keys.push(columns.flatMap((column) => (column.name === null ? [] : [column.name])));\n }\n\n return keys;\n })\n : dialect === \"mysql\"\n ? Effect.gen(function* () {\n const columns = yield* client`\n select column_name as name from information_schema.columns\n where table_schema = coalesce(${table.schema ?? null}, database())\n and table_name = ${table.name}\n `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgColumns)));\n\n for (const column of Object.values(table.columns)) {\n if (!columns.some((actual) => actual.name === column.name))\n return yield* PersistenceConfigurationError.make({\n reason: `Missing SQL column ${table.name}.${column.name}`,\n });\n }\n\n const indexes = yield* client`\n select index_name as name, column_name as columnName, sub_part as prefix\n from information_schema.statistics\n where table_schema = coalesce(${table.schema ?? null}, database())\n and table_name = ${table.name} and non_unique = 0\n order by index_name, seq_in_index\n `.pipe(\n Effect.flatMap(\n Schema.decodeUnknownEffect(\n Schema.Array(\n Schema.Struct({\n name: Schema.String,\n columnName: Schema.NullOr(Schema.String),\n prefix: Schema.NullOr(Schema.Number),\n }),\n ),\n ),\n ),\n );\n\n const keys = new Map<string, Array<(typeof indexes)[number]>>();\n\n for (const index of indexes) {\n const columns = keys.get(index.name) ?? [];\n\n columns.push(index);\n keys.set(index.name, columns);\n }\n\n return [...keys.values()]\n .filter((columns) =>\n columns.every((column) => column.columnName !== null && column.prefix === null),\n )\n .map((columns) =>\n columns.flatMap((column) =>\n column.columnName === null ? [] : [column.columnName],\n ),\n );\n })\n : Effect.gen(function* () {\n const relation = qualifiedTableName(table);\n\n const columns =\n yield* client`select attname as name from pg_attribute where attrelid = to_regclass(${relation}) and attnum > 0 and not attisdropped`.pipe(\n Effect.flatMap(Schema.decodeUnknownEffect(PgColumns)),\n );\n\n for (const column of Object.values(table.columns)) {\n if (!columns.some((actual) => actual.name === column.name))\n return yield* PersistenceConfigurationError.make({\n reason: `Missing SQL column ${relation}.${column.name}`,\n });\n }\n\n const indexes = yield* client`\n select array_agg(a.attname order by k.ordinality) as columns\n from pg_index i\n cross join lateral unnest(i.indkey) with ordinality as k(attnum, ordinality)\n join pg_attribute a on a.attrelid = i.indrelid and a.attnum = k.attnum\n where i.indrelid = to_regclass(${relation}) and i.indisunique and i.indisvalid\n and i.indimmediate and i.indpred is null and i.indexprs is null\n and k.ordinality <= i.indnkeyatts\n group by i.indexrelid\n `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgKeys)));\n\n return indexes.map((index) => index.columns);\n });\n\n for (const keys of required) {\n const columns = keys.map((key) => table.columns[key]?.name);\n\n if (\n columns.some((column) => column === undefined) ||\n !physicalKeys.some(\n // A stronger key also guarantees the required tuple's uniqueness.\n (actual) =>\n actual.length > 0 &&\n actual.length <= columns.length &&\n actual.every((column) => columns.includes(column)),\n )\n )\n return yield* PersistenceConfigurationError.make({\n reason: `Missing unique key on ${table.name} (${keys.join(\", \")})`,\n });\n }\n },\n (effect, _dialect, table) =>\n effect.pipe(\n Effect.mapError((error) =>\n Schema.is(PersistenceConfigurationError)(error)\n ? error\n : PersistenceConfigurationError.make({\n reason: `Cannot validate SQL storage for ${table.name}`,\n }),\n ),\n ),\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"storage-validation.mjs","names":[],"sources":["../../src/internal/storage-validation.ts"],"sourcesContent":["import { Context, Effect, Schema } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\n\nimport { PersistenceConfigurationError } from \"./configuration\";\nimport type { StorageTable } from \"./storage-tables\";\n\nconst Columns = Schema.Array(Schema.String);\nconst Keys = Schema.Array(Columns);\nconst Metadata = Schema.Struct({ relation: Schema.String, columns: Columns, keys: Keys });\nconst PgMetadata = Schema.Array(Metadata);\n\nconst SqliteMetadata = Schema.Array(\n Schema.Struct({\n relation: Schema.String,\n columns: Schema.fromJsonString(Columns),\n keys: Schema.fromJsonString(Keys),\n }),\n);\n\nconst MysqlColumns = Schema.Array(Schema.Struct({ relation: Schema.String, name: Schema.String }));\n\nconst MysqlIndexes = Schema.Array(\n Schema.Struct({\n relation: Schema.String,\n name: Schema.String,\n columnName: Schema.NullOr(Schema.String),\n prefix: Schema.NullOr(Schema.Number),\n }),\n);\n\nconst quote = (value: string) => '\"' + value.replaceAll('\"', '\"\"') + '\"';\n\n/** Physical metadata needed for catalog validation; value codecs remain adapter-owned. */\nexport interface PhysicalStorageTable {\n readonly name: string;\n readonly schema?: string;\n readonly columns: Readonly<Record<string, { readonly name: string }>>;\n}\n\nexport const qualifiedTableName = (table: Pick<StorageTable, \"name\" | \"schema\">) =>\n (table.schema === undefined ? \"\" : quote(table.schema) + \".\") + quote(table.name);\n\nconst validateKeys = Effect.fnUntraced(function* (\n table: PhysicalStorageTable,\n required: ReadonlyArray<ReadonlyArray<string>>,\n physicalKeys: ReadonlyArray<ReadonlyArray<string>>,\n) {\n for (const keys of required) {\n const columns = keys.map((key) => table.columns[key]?.name);\n\n if (\n columns.some((column) => column === undefined) ||\n !physicalKeys.some(\n // A stronger key also guarantees the required tuple's uniqueness.\n (actual) =>\n actual.length > 0 &&\n actual.length <= columns.length &&\n actual.every((column) => columns.includes(column)),\n )\n )\n return yield* PersistenceConfigurationError.make({\n reason: `Missing unique key on ${table.name} (${keys.join(\", \")})`,\n });\n }\n});\n\ninterface MetadataCache {\n active: boolean;\n readonly clients: Map<SqlClient, Map<string, typeof Metadata.Type>>;\n}\n\nconst CurrentMetadata = Context.Reference<MetadataCache | undefined>(\n \"@yielded/auth-persistence/StorageMetadata\",\n { defaultValue: () => undefined },\n);\n\n/** Share metadata only while constructing one service graph. Captured contexts\n * cannot reuse it after acquisition succeeds, fails, or is interrupted. */\nexport const withStorageValidation = Effect.fnUntraced(function* <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n): Effect.fn.Return<A, E, R> {\n const parent = yield* CurrentMetadata;\n\n if (parent?.active) return yield* effect;\n const cache: MetadataCache = { active: true, clients: new Map() };\n\n return yield* effect.pipe(\n Effect.provideService(CurrentMetadata, cache),\n Effect.ensuring(\n Effect.sync(() => {\n cache.active = false;\n cache.clients.clear();\n }),\n ),\n );\n});\n\n/** Requirements checked against one physical catalog snapshot. */\nexport interface StorageValidation {\n readonly table: PhysicalStorageTable;\n readonly required: ReadonlyArray<ReadonlyArray<string>>;\n}\n\n/** Batch physical metadata reads, reusing only the current acquisition snapshot.\n * Every requirement is still checked; later acquisitions read fresh metadata. */\nexport const validateStorageBatch = Effect.fnUntraced(\n function* (dialect: \"pg\" | \"mysql\" | \"sqlite\", requirements: ReadonlyArray<StorageValidation>) {\n if (requirements.length === 0) return;\n if (\n dialect === \"sqlite\" &&\n requirements.some(({ table }) => table.schema !== undefined && table.schema !== \"main\")\n )\n return yield* PersistenceConfigurationError.make({\n reason: \"SQLite storage must belong to the main database\",\n });\n const client = yield* SqlClient;\n const cache = yield* CurrentMetadata;\n\n const byRelation = cache?.active\n ? (cache.clients.get(client) ?? new Map<string, typeof Metadata.Type>())\n : new Map<string, typeof Metadata.Type>();\n\n if (cache?.active) cache.clients.set(client, byRelation);\n\n const tables = new Map(requirements.map(({ table }) => [qualifiedTableName(table), table]));\n\n const missing = [...tables.entries()].filter(\n ([relation]) => !byRelation.has(`${dialect}:${relation}`),\n );\n\n const relations = missing.map(([relation]) => relation);\n\n if (relations.length > 0) {\n const metadata = yield* dialect === \"sqlite\"\n ? Effect.gen(function* () {\n const requested = client.join(\n \", \",\n false,\n )(missing.map(([relation, table]) => client`(${relation}, ${table.name})`));\n\n return yield* client`\n with requested(relation, name) as (values ${requested})\n select requested.relation,\n (select json_group_array(name) from pragma_table_info(requested.name)) as columns,\n (select json_group_array(json(storage_key.columns)) from (\n select json_group_array(name) as columns from (\n select name from pragma_table_info(requested.name) where pk > 0 order by pk\n )\n union all\n select (select json_group_array(name) from pragma_index_info(i.name)) as columns\n from pragma_index_list(requested.name) i\n where i.\"unique\" = 1 and i.partial = 0\n and not exists(select 1 from pragma_index_info(i.name) where name is null)\n ) storage_key where json_array_length(storage_key.columns) > 0) as keys\n from requested\n `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(SqliteMetadata)));\n })\n : dialect === \"mysql\"\n ? Effect.gen(function* () {\n const requested = client.join(\n \" union all \",\n false,\n )(\n missing.map(\n ([relation, table]) =>\n client`select ${relation} as relation, ${table.schema ?? null} as schemaName, ${table.name} as tableName`,\n ),\n );\n\n const columns = yield* client`\n select requested.relation, c.column_name as name\n from (${requested}) requested join information_schema.columns c\n on c.table_schema = coalesce(requested.schemaName, database()) and c.table_name = requested.tableName\n `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(MysqlColumns)));\n\n const indexes = yield* client`\n select requested.relation, i.index_name as name, i.column_name as columnName, i.sub_part as prefix\n from (${requested}) requested join information_schema.statistics i\n on i.table_schema = coalesce(requested.schemaName, database()) and i.table_name = requested.tableName\n where i.non_unique = 0 order by requested.relation, i.index_name, i.seq_in_index\n `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(MysqlIndexes)));\n\n return relations.map((relation) => {\n const grouped = new Map<string, Array<(typeof indexes)[number]>>();\n\n for (const index of indexes) {\n if (index.relation !== relation) continue;\n const entries = grouped.get(index.name) ?? [];\n\n entries.push(index);\n grouped.set(index.name, entries);\n }\n\n return {\n relation,\n columns: columns\n .filter((column) => column.relation === relation)\n .map((column) => column.name),\n keys: [...grouped.values()]\n .filter((entries) =>\n entries.every((entry) => entry.columnName !== null && entry.prefix === null),\n )\n .map((entries) =>\n entries.flatMap((entry) =>\n entry.columnName === null ? [] : [entry.columnName],\n ),\n ),\n };\n });\n })\n : Effect.gen(function* () {\n const requested = client.join(\n \", \",\n false,\n )(relations.map((relation) => client`(${relation}::text)`));\n\n return yield* client`\n select requested.relation,\n array(select a.attname::text from pg_attribute a\n where a.attrelid = to_regclass(requested.relation)\n and a.attnum > 0 and not a.attisdropped) as columns,\n coalesce((select jsonb_agg(storage_key.columns) from (\n select array_agg(a.attname::text order by k.ordinality) as columns\n from pg_index i\n cross join lateral unnest(i.indkey) with ordinality as k(attnum, ordinality)\n join pg_attribute a on a.attrelid = i.indrelid and a.attnum = k.attnum\n where i.indrelid = to_regclass(requested.relation)\n and i.indisunique and i.indisvalid and i.indimmediate\n and i.indpred is null and i.indexprs is null\n and k.ordinality <= i.indnkeyatts\n group by i.indexrelid\n ) storage_key), '[]'::jsonb) as keys\n from (values ${requested}) requested(relation)\n `.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PgMetadata)));\n });\n\n for (const row of metadata) byRelation.set(`${dialect}:${row.relation}`, row);\n }\n\n for (const { table, required } of requirements) {\n const relation = qualifiedTableName(table);\n const actual = byRelation.get(`${dialect}:${relation}`);\n\n for (const column of Object.values(table.columns)) {\n if (actual === undefined || !actual.columns.includes(column.name))\n return yield* PersistenceConfigurationError.make({\n reason: `Missing SQL column ${relation}.${column.name}`,\n });\n }\n yield* validateKeys(table, required, actual?.keys ?? []);\n }\n },\n Effect.mapError((error) =>\n Schema.is(PersistenceConfigurationError)(error)\n ? error\n : PersistenceConfigurationError.make({ reason: \"Cannot validate SQL storage metadata\" }),\n ),\n);\n\n/** Validate physical columns and unconditional uniqueness before operations run. */\nexport const validateStorage = (\n dialect: \"pg\" | \"mysql\" | \"sqlite\",\n table: PhysicalStorageTable,\n required: ReadonlyArray<ReadonlyArray<string>>,\n) => validateStorageBatch(dialect, [{ table, required }]);\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { NativeDatabase, TransactionNativeDatabase, TransactionOwner, makeTransactionKernel } from "./transaction-kernel.mjs";
2
- import { Context, Effect } from "effect";
2
+ import { Context, Crypto, Effect, PlatformError } from "effect";
3
3
  import { CurrentCommitJournal, HookConfigurationError, LifecycleHooks } from "@yielded/auth/Hooks";
4
4
  import * as SqlClient$1 from "effect/sql/SqlClient";
5
5
  import { Statement } from "effect/sql/Statement";
@@ -26,12 +26,12 @@ export interface TransactionExecution<Failure, OwnerId, Requirements = Lifecycle
26
26
  readonly active: () => boolean;
27
27
  readonly admit: Effect.Effect<void, Failure>;
28
28
  readonly poison: () => void;
29
- readonly run: <A, E, R>(body: Effect.Effect<A, E, R>, mutation?: boolean) => Effect.Effect<A, Failure, Exclude<R, OwnerId | CurrentCommitJournal> | Requirements>;
29
+ readonly run: <A, E, R>(body: Effect.Effect<A, E, R>, mutation?: boolean) => Effect.Effect<A, Failure, Exclude<Exclude<R, OwnerId | CurrentCommitJournal>, Crypto.Crypto> | Requirements>;
30
30
  }
31
31
  export declare const makeTransactionExecutionKernel: (transactions: Pick<ReturnType<typeof makeTransactionKernel>, "makeTransactionOwner" | "reportTransactionFailure">) => {
32
32
  sqlClientTransactionStandaloneGuard: <Failure>(unavailable: () => Failure, transactionService: SqlClient$1.SqlClient["transactionService"] | undefined) => Effect.Effect<void, Failure, never>;
33
- makeTransactionExecution: <Failure, OwnerId>(ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>, configuration: TransactionTargetConfiguration<Failure>, unavailable: () => Failure, nonce: () => string, bound?: TransactionBound<Failure> | undefined) => Effect.Effect<TransactionExecution<Failure, OwnerId, LifecycleHooks>, never, NativeDatabase>;
34
- coordinateTransactionOwner: <Failure, OwnerId, Resources, AllocationError, AllocationRequirements, Services, Transaction, A, E, R>(database: TransactionNativeDatabase, ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>, configuration: TransactionTargetConfiguration<Failure>, allocate: Effect.Effect<Resources, AllocationError, AllocationRequirements>, unavailable: () => Failure, nonce: () => string, services: (execution: TransactionExecution<Failure, OwnerId>, resources: Resources) => Services, owner: (transaction: Transaction, services: Services, append: (statement: Statement<any>) => void) => Effect.Effect<A, E, R>) => Effect.Effect<A, TransactionCoordinatorError<E, Failure>, Exclude<R, CurrentCommitJournal> | LifecycleHooks | AllocationRequirements>;
33
+ makeTransactionExecution: <Failure, OwnerId>(ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>, configuration: TransactionTargetConfiguration<Failure>, unavailable: () => Failure, nonce: Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>, bound?: TransactionBound<Failure> | undefined) => Effect.Effect<TransactionExecution<Failure, OwnerId, LifecycleHooks>, never, Crypto.Crypto | NativeDatabase>;
34
+ coordinateTransactionOwner: <Failure, OwnerId, Resources, AllocationError, AllocationRequirements, Services, Transaction, A, E, R>(database: TransactionNativeDatabase, ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>, configuration: TransactionTargetConfiguration<Failure>, allocate: Effect.Effect<Resources, AllocationError, AllocationRequirements>, unavailable: () => Failure, nonce: Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>, services: (execution: TransactionExecution<Failure, OwnerId>, resources: Resources) => Services, owner: (transaction: Transaction, services: Services, append: (statement: Statement<any>) => void) => Effect.Effect<A, E, R>) => Effect.Effect<A, TransactionCoordinatorError<E, Failure>, Exclude<R, CurrentCommitJournal> | LifecycleHooks | AllocationRequirements | Crypto.Crypto>;
35
35
  };
36
36
  //#endregion
37
37
  //# sourceMappingURL=transaction-execution-kernel.d.mts.map
@@ -1,6 +1,6 @@
1
1
  import { requireStandalone } from "./standalone.mjs";
2
2
  import { NativeDatabase } from "./transaction-kernel.mjs";
3
- import { Context, Effect } from "effect";
3
+ import { Context, Crypto, Effect } from "effect";
4
4
  import { CurrentCommitJournal, coordinateCommit, hasCommitScope } from "@yielded/auth/Hooks";
5
5
  //#region src/internal/transaction-execution-kernel.ts
6
6
  const makeTransactionExecutionKernel = (transactions) => {
@@ -8,6 +8,7 @@ const makeTransactionExecutionKernel = (transactions) => {
8
8
  const sqlClientTransactionStandaloneGuard = (unavailable, transactionService) => requireStandalone(unavailable, transactionService);
9
9
  const makeTransactionExecution = Effect.fnUntraced(function* (ownerTag, configuration, unavailable, nonce, bound) {
10
10
  const database = yield* NativeDatabase;
11
+ const crypto = yield* Crypto.Crypto;
11
12
  const invariant = (value) => {
12
13
  if (!value) throw unavailable();
13
14
  };
@@ -44,7 +45,7 @@ const makeTransactionExecutionKernel = (transactions) => {
44
45
  }))) : Effect.gen(function* () {
45
46
  if (yield* hasCommitScope) return yield* Effect.fail(unavailable());
46
47
  yield* configuration.standaloneGuard(database.$client.transactionService);
47
- const marker = nonce();
48
+ const marker = yield* nonce;
48
49
  return (yield* coordinateCommit((journal) => {
49
50
  const run = (tx) => Effect.gen(function* () {
50
51
  const owner = makeTransactionOwner(tx, journal, marker, unavailable, {
@@ -61,7 +62,7 @@ const makeTransactionExecutionKernel = (transactions) => {
61
62
  });
62
63
  return configuration.mode === "batch" ? run(database) : configuration.transaction !== void 0 ? configuration.transaction(database, run) : database.transaction(run);
63
64
  }, { mode: configuration.mode })).value;
64
- })).pipe((operation) => reportTransactionFailure(operation, unavailable), Effect.mapError(unavailable), Effect.catchDefect(() => Effect.fail(unavailable())));
65
+ })).pipe(Effect.provideService(Crypto.Crypto, crypto), (operation) => reportTransactionFailure(operation, unavailable), Effect.mapError(unavailable), Effect.catchDefect(() => Effect.fail(unavailable())));
65
66
  });
66
67
  }
67
68
  };
@@ -70,8 +71,9 @@ const makeTransactionExecutionKernel = (transactions) => {
70
71
  return Effect.gen(function* () {
71
72
  if (yield* hasCommitScope) return yield* Effect.fail(unavailable());
72
73
  yield* configuration.standaloneGuard(database.$client.transactionService);
74
+ const crypto = yield* Crypto.Crypto;
73
75
  const resources = yield* allocate.pipe((operation) => reportTransactionFailure(operation, unavailable), Effect.mapError(unavailable), Effect.catchDefect(() => Effect.fail(unavailable())));
74
- const marker = nonce();
76
+ const marker = yield* nonce.pipe(Effect.mapError(unavailable));
75
77
  return (yield* coordinateCommit((journal) => {
76
78
  const run = (tx) => Effect.gen(function* () {
77
79
  const state = makeTransactionOwner(tx, journal, marker, unavailable, {
@@ -101,7 +103,7 @@ const makeTransactionExecutionKernel = (transactions) => {
101
103
  }).pipe(Effect.ensuring(Effect.sync(() => {
102
104
  bound.active = false;
103
105
  })));
104
- });
106
+ }).pipe(Effect.provideService(Crypto.Crypto, crypto));
105
107
  return configuration.mode === "batch" ? run(database) : configuration.transaction !== void 0 ? configuration.transaction(database, run) : database.transaction(run);
106
108
  }, { mode: configuration.mode })).value;
107
109
  });
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-execution-kernel.mjs","names":[],"sources":["../../src/internal/transaction-execution-kernel.ts"],"sourcesContent":["import {\n coordinateCommit,\n hasCommitScope,\n CurrentCommitJournal,\n type LifecycleHooks,\n type HookConfigurationError,\n} from \"@yielded/auth/Hooks\";\n/* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */\n/* oxlint-disable no-explicit-any -- private execution bridge retains exact public driver wrappers. */\nimport { Context, Effect } from \"effect\";\nimport type * as SqlClient from \"effect/sql/SqlClient\";\nimport type * as SqlError from \"effect/sql/SqlError\";\nimport type { Statement } from \"effect/sql/Statement\";\n\nimport { requireStandalone } from \"./standalone\";\nimport {\n NativeDatabase,\n type TransactionNativeDatabase,\n type TransactionOwner,\n type makeTransactionKernel,\n} from \"./transaction-kernel\";\n\nexport interface TransactionTargetConfiguration<Failure> {\n readonly mode: \"interactive\" | \"synchronous\" | \"batch\";\n readonly dialect: \"pg\" | \"mysql\" | \"sqlite\";\n readonly locking: boolean;\n /** The captured database's transaction marker; batch drivers may omit it. */\n readonly standaloneGuard: (\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => Effect.Effect<void, Failure>;\n readonly transaction?: <A, E, R>(\n database: any,\n body: (transaction: any) => Effect.Effect<A, E, R>,\n ) => Effect.Effect<A, E | SqlError.SqlError | Failure, R>;\n}\n\nexport type TransactionCoordinatorError<E, Failure> =\n | E\n | Failure\n | HookConfigurationError\n | SqlError.SqlError;\n\nexport interface TransactionBound<Failure> {\n readonly owner: TransactionOwner<Failure>;\n used: boolean;\n poisoned: boolean;\n active: boolean;\n inFlight: number;\n}\n\nexport interface TransactionExecution<Failure, OwnerId, Requirements = LifecycleHooks> {\n readonly bound: boolean;\n readonly active: () => boolean;\n readonly admit: Effect.Effect<void, Failure>;\n readonly poison: () => void;\n readonly run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation?: boolean,\n ) => Effect.Effect<A, Failure, Exclude<R, OwnerId | CurrentCommitJournal> | Requirements>;\n}\n\nexport const makeTransactionExecutionKernel = (\n transactions: Pick<\n ReturnType<typeof makeTransactionKernel>,\n \"makeTransactionOwner\" | \"reportTransactionFailure\"\n >,\n) => {\n const { makeTransactionOwner, reportTransactionFailure } = transactions;\n\n const sqlClientTransactionStandaloneGuard = <Failure>(\n unavailable: () => Failure,\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => requireStandalone(unavailable, transactionService);\n\n const makeTransactionExecution = Effect.fnUntraced(function* <Failure, OwnerId>(\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n unavailable: () => Failure,\n nonce: () => string,\n bound?: TransactionBound<Failure>,\n ): Effect.fn.Return<TransactionExecution<Failure, OwnerId>, never, NativeDatabase> {\n const database = yield* NativeDatabase;\n\n const invariant: (value: unknown) => asserts value = (value) => {\n if (!value) throw unavailable();\n };\n\n return {\n bound: bound !== undefined,\n active: () => bound === undefined || bound.active,\n admit:\n bound !== undefined\n ? Effect.suspend(() => (bound.active ? Effect.void : Effect.fail(unavailable())))\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n }),\n poison: () => {\n if (bound !== undefined) bound.poisoned = true;\n },\n run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation = true,\n ): Effect.Effect<A, Failure, Exclude<R, OwnerId | CurrentCommitJournal> | LifecycleHooks> => {\n return Effect.suspend(() => {\n let entered = false;\n\n const work =\n bound !== undefined\n ? Effect.gen(function* () {\n if (!bound.active) return yield* Effect.fail(unavailable());\n entered = true;\n bound.inFlight++;\n if (!mutation)\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, bound.owner).pipe(\n Context.add(CurrentCommitJournal, bound.owner.journal),\n ),\n );\n invariant(!bound.used && !bound.poisoned);\n bound.used = true;\n\n const result = yield* coordinateCommit(\n (journal) =>\n Effect.gen(function* () {\n bound.owner.guards.push(journal.prepare(undefined));\n\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, { ...bound.owner, journal }).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n }),\n { mode: configuration.mode },\n );\n\n return result.value;\n }).pipe(\n Effect.onExit((exit) =>\n Effect.sync(() => {\n if (entered) bound.inFlight--;\n if (exit._tag === \"Failure\") bound.poisoned = true;\n }),\n ),\n )\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n const marker = nonce();\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const owner = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n locking: configuration.locking,\n mysql: configuration.dialect === \"mysql\",\n dialect: configuration.dialect,\n });\n\n const value = yield* Effect.provideContext(\n body,\n Context.make(ownerTag, owner).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n\n yield* owner.finish();\n if (configuration.mode === \"batch\")\n yield* tx.$client.batch(owner.statements);\n\n return value;\n });\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value;\n });\n\n return work.pipe(\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n });\n },\n };\n });\n\n const coordinateTransactionOwner = <\n Failure,\n OwnerId,\n Resources,\n AllocationError,\n AllocationRequirements,\n Services,\n Transaction,\n A,\n E,\n R,\n >(\n database: TransactionNativeDatabase,\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n allocate: Effect.Effect<Resources, AllocationError, AllocationRequirements>,\n unavailable: () => Failure,\n nonce: () => string,\n services: (execution: TransactionExecution<Failure, OwnerId>, resources: Resources) => Services,\n owner: (\n transaction: Transaction,\n services: Services,\n append: (statement: Statement<any>) => void,\n ) => Effect.Effect<A, E, R>,\n ): Effect.Effect<\n A,\n TransactionCoordinatorError<E, Failure>,\n Exclude<R, CurrentCommitJournal> | LifecycleHooks | AllocationRequirements\n > => {\n return Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n\n // Allocation is independent of registration data and always precedes the physical owner.\n const resources = yield* allocate.pipe(\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n\n const marker = nonce();\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const state = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n dialect: configuration.dialect,\n mysql: configuration.dialect === \"mysql\",\n locking: configuration.locking,\n });\n\n const bound: TransactionBound<Failure> = {\n owner: state,\n used: false,\n poisoned: false,\n active: true,\n inFlight: 0,\n };\n\n return yield* Effect.gen(function* () {\n const value = yield* owner(\n tx as Transaction,\n services(\n yield* makeTransactionExecution(\n ownerTag,\n configuration,\n unavailable,\n nonce,\n bound,\n ).pipe(Effect.provideService(NativeDatabase, tx)),\n resources,\n ),\n (statement) => {\n if (!bound.active) throw unavailable();\n state.statements.push(statement);\n },\n );\n\n bound.active = false;\n if (bound.poisoned || bound.inFlight !== 0)\n return yield* Effect.fail(unavailable());\n yield* state.finish();\n if (configuration.mode === \"batch\") yield* tx.$client.batch(state.statements);\n\n return value;\n }).pipe(\n Effect.ensuring(\n Effect.sync(() => {\n bound.active = false;\n }),\n ),\n );\n });\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value as A;\n });\n };\n\n return {\n sqlClientTransactionStandaloneGuard,\n makeTransactionExecution,\n coordinateTransactionOwner,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"transaction-execution-kernel.mjs","names":[],"sources":["../../src/internal/transaction-execution-kernel.ts"],"sourcesContent":["import {\n coordinateCommit,\n hasCommitScope,\n CurrentCommitJournal,\n type LifecycleHooks,\n type HookConfigurationError,\n} from \"@yielded/auth/Hooks\";\n/* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */\n/* oxlint-disable no-explicit-any -- private execution bridge retains exact public driver wrappers. */\nimport { Context, Crypto, Effect, type PlatformError } from \"effect\";\nimport type * as SqlClient from \"effect/sql/SqlClient\";\nimport type * as SqlError from \"effect/sql/SqlError\";\nimport type { Statement } from \"effect/sql/Statement\";\n\nimport { requireStandalone } from \"./standalone\";\nimport {\n NativeDatabase,\n type TransactionNativeDatabase,\n type TransactionOwner,\n type makeTransactionKernel,\n} from \"./transaction-kernel\";\n\nexport interface TransactionTargetConfiguration<Failure> {\n readonly mode: \"interactive\" | \"synchronous\" | \"batch\";\n readonly dialect: \"pg\" | \"mysql\" | \"sqlite\";\n readonly locking: boolean;\n /** The captured database's transaction marker; batch drivers may omit it. */\n readonly standaloneGuard: (\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => Effect.Effect<void, Failure>;\n readonly transaction?: <A, E, R>(\n database: any,\n body: (transaction: any) => Effect.Effect<A, E, R>,\n ) => Effect.Effect<A, E | SqlError.SqlError | Failure, R>;\n}\n\nexport type TransactionCoordinatorError<E, Failure> =\n | E\n | Failure\n | HookConfigurationError\n | SqlError.SqlError;\n\nexport interface TransactionBound<Failure> {\n readonly owner: TransactionOwner<Failure>;\n used: boolean;\n poisoned: boolean;\n active: boolean;\n inFlight: number;\n}\n\nexport interface TransactionExecution<Failure, OwnerId, Requirements = LifecycleHooks> {\n readonly bound: boolean;\n readonly active: () => boolean;\n readonly admit: Effect.Effect<void, Failure>;\n readonly poison: () => void;\n readonly run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation?: boolean,\n ) => Effect.Effect<\n A,\n Failure,\n Exclude<Exclude<R, OwnerId | CurrentCommitJournal>, Crypto.Crypto> | Requirements\n >;\n}\n\nexport const makeTransactionExecutionKernel = (\n transactions: Pick<\n ReturnType<typeof makeTransactionKernel>,\n \"makeTransactionOwner\" | \"reportTransactionFailure\"\n >,\n) => {\n const { makeTransactionOwner, reportTransactionFailure } = transactions;\n\n const sqlClientTransactionStandaloneGuard = <Failure>(\n unavailable: () => Failure,\n transactionService: SqlClient.SqlClient[\"transactionService\"] | undefined,\n ) => requireStandalone(unavailable, transactionService);\n\n const makeTransactionExecution = Effect.fnUntraced(function* <Failure, OwnerId>(\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n unavailable: () => Failure,\n nonce: Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>,\n bound?: TransactionBound<Failure>,\n ): Effect.fn.Return<\n TransactionExecution<Failure, OwnerId>,\n never,\n NativeDatabase | Crypto.Crypto\n > {\n const database = yield* NativeDatabase;\n const crypto = yield* Crypto.Crypto;\n\n const invariant: (value: unknown) => asserts value = (value) => {\n if (!value) throw unavailable();\n };\n\n return {\n bound: bound !== undefined,\n active: () => bound === undefined || bound.active,\n admit:\n bound !== undefined\n ? Effect.suspend(() => (bound.active ? Effect.void : Effect.fail(unavailable())))\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n }),\n poison: () => {\n if (bound !== undefined) bound.poisoned = true;\n },\n run: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n mutation = true,\n ): Effect.Effect<\n A,\n Failure,\n Exclude<Exclude<R, OwnerId | CurrentCommitJournal>, Crypto.Crypto> | LifecycleHooks\n > => {\n return Effect.suspend(() => {\n let entered = false;\n\n const work: Effect.Effect<\n A,\n E | Failure | HookConfigurationError | SqlError.SqlError | PlatformError.PlatformError,\n Exclude<R, OwnerId | CurrentCommitJournal> | LifecycleHooks | Crypto.Crypto\n > =\n bound !== undefined\n ? Effect.gen(function* () {\n if (!bound.active) return yield* Effect.fail(unavailable());\n entered = true;\n bound.inFlight++;\n if (!mutation)\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, bound.owner).pipe(\n Context.add(CurrentCommitJournal, bound.owner.journal),\n ),\n );\n invariant(!bound.used && !bound.poisoned);\n bound.used = true;\n\n const result = yield* coordinateCommit(\n (journal) =>\n Effect.gen(function* () {\n bound.owner.guards.push(journal.prepare(undefined));\n\n return yield* Effect.provideContext(\n body,\n Context.make(ownerTag, { ...bound.owner, journal }).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n }),\n { mode: configuration.mode },\n );\n\n return result.value;\n }).pipe(\n Effect.onExit((exit) =>\n Effect.sync(() => {\n if (entered) bound.inFlight--;\n if (exit._tag === \"Failure\") bound.poisoned = true;\n }),\n ),\n )\n : Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n const marker = yield* nonce;\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const owner = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n locking: configuration.locking,\n mysql: configuration.dialect === \"mysql\",\n dialect: configuration.dialect,\n });\n\n const value = yield* Effect.provideContext(\n body,\n Context.make(ownerTag, owner).pipe(\n Context.add(CurrentCommitJournal, journal),\n ),\n );\n\n yield* owner.finish();\n if (configuration.mode === \"batch\")\n yield* tx.$client.batch(owner.statements);\n\n return value;\n });\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value;\n });\n\n return work.pipe(\n Effect.provideService(Crypto.Crypto, crypto),\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n });\n },\n };\n });\n\n const coordinateTransactionOwner = <\n Failure,\n OwnerId,\n Resources,\n AllocationError,\n AllocationRequirements,\n Services,\n Transaction,\n A,\n E,\n R,\n >(\n database: TransactionNativeDatabase,\n ownerTag: Context.Key<OwnerId, TransactionOwner<Failure>>,\n configuration: TransactionTargetConfiguration<Failure>,\n allocate: Effect.Effect<Resources, AllocationError, AllocationRequirements>,\n unavailable: () => Failure,\n nonce: Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>,\n services: (execution: TransactionExecution<Failure, OwnerId>, resources: Resources) => Services,\n owner: (\n transaction: Transaction,\n services: Services,\n append: (statement: Statement<any>) => void,\n ) => Effect.Effect<A, E, R>,\n ): Effect.Effect<\n A,\n TransactionCoordinatorError<E, Failure>,\n Exclude<R, CurrentCommitJournal> | LifecycleHooks | AllocationRequirements | Crypto.Crypto\n > => {\n return Effect.gen(function* () {\n if (yield* hasCommitScope) return yield* Effect.fail(unavailable());\n yield* configuration.standaloneGuard(database.$client.transactionService);\n const crypto = yield* Crypto.Crypto;\n\n // Allocation is independent of registration data and always precedes the physical owner.\n const resources = yield* allocate.pipe(\n (operation) => reportTransactionFailure(operation, unavailable),\n Effect.mapError(unavailable),\n Effect.catchDefect(() => Effect.fail(unavailable())),\n );\n\n const marker = yield* nonce.pipe(Effect.mapError(unavailable));\n\n const result = yield* coordinateCommit(\n (journal) => {\n const run = (tx: TransactionNativeDatabase) =>\n Effect.gen(function* () {\n const state = makeTransactionOwner(tx, journal, marker, unavailable, {\n client: database.$client,\n batch: configuration.mode === \"batch\",\n dialect: configuration.dialect,\n mysql: configuration.dialect === \"mysql\",\n locking: configuration.locking,\n });\n\n const bound: TransactionBound<Failure> = {\n owner: state,\n used: false,\n poisoned: false,\n active: true,\n inFlight: 0,\n };\n\n return yield* Effect.gen(function* () {\n const value = yield* owner(\n tx as Transaction,\n services(\n yield* makeTransactionExecution(\n ownerTag,\n configuration,\n unavailable,\n nonce,\n bound,\n ).pipe(Effect.provideService(NativeDatabase, tx)),\n resources,\n ),\n (statement) => {\n if (!bound.active) throw unavailable();\n state.statements.push(statement);\n },\n );\n\n bound.active = false;\n if (bound.poisoned || bound.inFlight !== 0)\n return yield* Effect.fail(unavailable());\n yield* state.finish();\n if (configuration.mode === \"batch\") yield* tx.$client.batch(state.statements);\n\n return value;\n }).pipe(\n Effect.ensuring(\n Effect.sync(() => {\n bound.active = false;\n }),\n ),\n );\n }).pipe(Effect.provideService(Crypto.Crypto, crypto));\n\n return configuration.mode === \"batch\"\n ? run(database)\n : configuration.transaction !== undefined\n ? configuration.transaction(database, run)\n : database.transaction(run);\n },\n { mode: configuration.mode },\n );\n\n return result.value as A;\n });\n };\n\n return {\n sqlClientTransactionStandaloneGuard,\n makeTransactionExecution,\n coordinateTransactionOwner,\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@yielded/auth-persistence","version":"0.1.0-beta.17","dependencies":{"@noble/hashes":"2.3.0","@yielded/auth":"0.1.0-beta.17","@yielded/auth-crypto":"0.1.0-beta.17"},"devDependencies":{"@effect/platform-node":"4.0.0","@effect/sql-libsql":"4.0.0","@effect/sql-sqlite-node":"4.0.0","@effect/vitest":"4.0.0","effect":"4.0.0","typescript":"7.0.2","vite-plus":"0.3.3"},"peerDependencies":{"effect":"^4.0.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./Adapter":{"types":"./dist/Adapter.d.mts","default":"./dist/Adapter.mjs"}},"description":"Direct Effect SQL persistence and shared storage contracts for Yielded Auth.","homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/auth-persistence"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
1
+ {"name":"@yielded/auth-persistence","version":"0.1.0-beta.19","dependencies":{"@yielded/auth":"0.1.0-beta.19"},"devDependencies":{"@effect/platform-node":"4.0.0","@effect/sql-libsql":"4.0.0","@effect/sql-sqlite-node":"4.0.0","@effect/vitest":"4.0.0","effect":"4.0.0","typescript":"7.0.2","vite-plus":"0.3.3"},"peerDependencies":{"effect":"^4.0.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./Adapter":{"types":"./dist/Adapter.d.mts","default":"./dist/Adapter.mjs"}},"description":"Direct Effect SQL persistence and shared storage contracts for Yielded Auth.","homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/auth-persistence"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
package/src/Adapter.ts CHANGED
@@ -297,4 +297,10 @@ export type {
297
297
  PersistenceApi,
298
298
  } from "./internal/configuration";
299
299
 
300
- export { validateStorage, type PhysicalStorageTable } from "./internal/storage-validation";
300
+ export {
301
+ validateStorage,
302
+ validateStorageBatch,
303
+ withStorageValidation,
304
+ type PhysicalStorageTable,
305
+ type StorageValidation,
306
+ } from "./internal/storage-validation";
@@ -24,7 +24,7 @@ import type {
24
24
  AuthenticationRequirement,
25
25
  make as makeSessions,
26
26
  } from "@yielded/auth/Sessions";
27
- import { type Context, type Effect, type Layer, Schema } from "effect";
27
+ import { type Context, type Crypto, type Effect, type Layer, Schema } from "effect";
28
28
  import type { SqlClient } from "effect/sql";
29
29
 
30
30
  import type { PersistenceMappingError } from "./mapping-error";
@@ -62,7 +62,7 @@ export interface Strategy {
62
62
  export interface Definition<C extends ClaimsCodec, Id extends string> {
63
63
  readonly namespace: string;
64
64
  readonly claims: C;
65
- readonly sessionMode?: string;
65
+ readonly sessionMode?: string | undefined;
66
66
  readonly sessions: ReturnType<typeof makeSessions<C, Id>>;
67
67
  readonly strategies: Readonly<Record<string, Strategy>>;
68
68
  }
@@ -251,6 +251,7 @@ export interface BoundPersistence<
251
251
  readonly layer: Layer.Layer<
252
252
  Ports<C, Id, A>,
253
253
  PersistenceConfigurationError,
254
+ | Crypto.Crypto
254
255
  | ConfigId<A["namespace"]>
255
256
  | ProvisioningRequirement<A>
256
257
  | PasskeyRequirement<A>
@@ -260,7 +261,8 @@ export interface BoundPersistence<
260
261
  readonly managed: <N, Instant = number>(options: {
261
262
  readonly subjects: SubjectOptions<T, N>;
262
263
  readonly tables?: Partial<Record<Roles<C, Id, A>, T>>;
263
- readonly prefix?: string;
264
+ /** Stable SQL identifier prefix; reuse the deployed prefix for existing tables. */
265
+ readonly prefix: string;
264
266
  readonly timestamps?: TimestampOptions<Instant>;
265
267
  }) => StorageLayout<T, Roles<C, Id, A>>;
266
268
  readonly map: <N, Instant = number>(options: {
@@ -0,0 +1,15 @@
1
+ import { TokenDigest } from "@yielded/auth/Schema";
2
+ import { Crypto, Effect } from "effect";
3
+ import { Base64Url } from "effect/encoding";
4
+
5
+ const encoder = new TextEncoder();
6
+
7
+ export const randomId = Effect.flatMap(Crypto.Crypto, (crypto) =>
8
+ crypto.randomBytes(32).pipe(Effect.map(Base64Url.encode)),
9
+ );
10
+
11
+ /** Persisted UTF-8 SHA-256, encoded as unpadded base64url. */
12
+ export const digest = (value: string) =>
13
+ Effect.flatMap(Crypto.Crypto, (crypto) => crypto.digest("SHA-256", encoder.encode(value))).pipe(
14
+ Effect.map((bytes) => TokenDigest.make(Base64Url.encode(bytes))),
15
+ );