@yielded/auth-persistence 0.1.0-beta.22 → 0.1.0-beta.24

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 (182) hide show
  1. package/README.md +11 -0
  2. package/dist/Adapter.d.mts +13 -5
  3. package/dist/Adapter.mjs +9 -2
  4. package/dist/OAuthPersistence.d.mts +87 -0
  5. package/dist/OAuthPersistence.mjs +125 -0
  6. package/dist/OAuthPersistence.mjs.map +1 -0
  7. package/dist/_virtual/_rolldown/runtime.mjs +13 -0
  8. package/dist/index.d.mts +3 -1
  9. package/dist/index.mjs +3 -1
  10. package/dist/internal/authority-snapshot.mjs +53 -0
  11. package/dist/internal/authority-snapshot.mjs.map +1 -0
  12. package/dist/internal/configuration.d.mts +1 -1
  13. package/dist/internal/configuration.mjs.map +1 -1
  14. package/dist/internal/models/oauth-connected-model.d.mts +294 -0
  15. package/dist/internal/models/oauth-connected-model.mjs +22 -0
  16. package/dist/internal/models/oauth-connected-model.mjs.map +1 -0
  17. package/dist/internal/models/oauth-model.d.mts +467 -0
  18. package/dist/internal/models/oauth-model.mjs +49 -0
  19. package/dist/internal/models/oauth-model.mjs.map +1 -0
  20. package/dist/internal/models/password-model.d.mts +4 -97
  21. package/dist/internal/models/password-model.mjs +0 -3
  22. package/dist/internal/models/password-model.mjs.map +1 -1
  23. package/dist/internal/native-sql-table.d.mts +34 -0
  24. package/dist/internal/native-sql-table.mjs +111 -0
  25. package/dist/internal/native-sql-table.mjs.map +1 -0
  26. package/dist/internal/oauth/accounts.d.mts +8 -0
  27. package/dist/internal/oauth/accounts.mjs +379 -0
  28. package/dist/internal/oauth/accounts.mjs.map +1 -0
  29. package/dist/internal/oauth/connected-access.d.mts +9 -0
  30. package/dist/internal/oauth/connected-access.mjs +229 -0
  31. package/dist/internal/oauth/connected-access.mjs.map +1 -0
  32. package/dist/internal/oauth/connected-collection.d.mts +7 -0
  33. package/dist/internal/oauth/connected-collection.mjs +214 -0
  34. package/dist/internal/oauth/connected-collection.mjs.map +1 -0
  35. package/dist/internal/oauth/connected-custody.d.mts +6 -0
  36. package/dist/internal/oauth/connected-custody.mjs +165 -0
  37. package/dist/internal/oauth/connected-custody.mjs.map +1 -0
  38. package/dist/internal/oauth/connected-flow.d.mts +7 -0
  39. package/dist/internal/oauth/connected-flow.mjs +324 -0
  40. package/dist/internal/oauth/connected-flow.mjs.map +1 -0
  41. package/dist/internal/oauth/connected-input.mjs +122 -0
  42. package/dist/internal/oauth/connected-input.mjs.map +1 -0
  43. package/dist/internal/oauth/connected-keys.d.mts +2 -0
  44. package/dist/internal/oauth/connected-keys.mjs +22 -0
  45. package/dist/internal/oauth/connected-keys.mjs.map +1 -0
  46. package/dist/internal/oauth/connected-maintenance.d.mts +9 -0
  47. package/dist/internal/oauth/connected-maintenance.mjs +226 -0
  48. package/dist/internal/oauth/connected-maintenance.mjs.map +1 -0
  49. package/dist/internal/oauth/connected-management.d.mts +9 -0
  50. package/dist/internal/oauth/connected-management.mjs +243 -0
  51. package/dist/internal/oauth/connected-management.mjs.map +1 -0
  52. package/dist/internal/oauth/connected-reference.mjs +63 -0
  53. package/dist/internal/oauth/connected-reference.mjs.map +1 -0
  54. package/dist/internal/oauth/connected-settlement.d.mts +8 -0
  55. package/dist/internal/oauth/connected-settlement.mjs +141 -0
  56. package/dist/internal/oauth/connected-settlement.mjs.map +1 -0
  57. package/dist/internal/oauth/connected-sign-in.d.mts +10 -0
  58. package/dist/internal/oauth/connected-sign-in.mjs +281 -0
  59. package/dist/internal/oauth/connected-sign-in.mjs.map +1 -0
  60. package/dist/internal/oauth/connected-state.d.mts +14 -0
  61. package/dist/internal/oauth/connected-state.mjs +469 -0
  62. package/dist/internal/oauth/connected-state.mjs.map +1 -0
  63. package/dist/internal/oauth/connected-target.d.mts +21 -0
  64. package/dist/internal/oauth/connected-target.mjs +117 -0
  65. package/dist/internal/oauth/connected-target.mjs.map +1 -0
  66. package/dist/internal/oauth/flow.d.mts +5 -0
  67. package/dist/internal/oauth/flow.mjs +279 -0
  68. package/dist/internal/oauth/flow.mjs.map +1 -0
  69. package/dist/internal/oauth/input.mjs +127 -0
  70. package/dist/internal/oauth/input.mjs.map +1 -0
  71. package/dist/internal/oauth/owner.d.mts +10 -0
  72. package/dist/internal/oauth/owner.mjs +10 -0
  73. package/dist/internal/oauth/owner.mjs.map +1 -0
  74. package/dist/internal/oauth/query-compiler.d.mts +7 -0
  75. package/dist/internal/oauth/query-compiler.mjs +9 -0
  76. package/dist/internal/oauth/query-compiler.mjs.map +1 -0
  77. package/dist/internal/oauth/registration.d.mts +7 -0
  78. package/dist/internal/oauth/registration.mjs +393 -0
  79. package/dist/internal/oauth/registration.mjs.map +1 -0
  80. package/dist/internal/oauth/state.mjs +107 -0
  81. package/dist/internal/oauth/state.mjs.map +1 -0
  82. package/dist/internal/oauth/target.d.mts +31 -0
  83. package/dist/internal/oauth/target.mjs +273 -0
  84. package/dist/internal/oauth/target.mjs.map +1 -0
  85. package/dist/internal/oauth-kernel.d.mts +1423 -0
  86. package/dist/internal/oauth-kernel.mjs +187 -0
  87. package/dist/internal/oauth-kernel.mjs.map +1 -0
  88. package/dist/internal/oauth-proxy.d.mts +31 -0
  89. package/dist/internal/oauth-proxy.mjs +129 -0
  90. package/dist/internal/oauth-proxy.mjs.map +1 -0
  91. package/dist/internal/passkey/target.d.mts +1 -1
  92. package/dist/internal/passkey-kernel.d.mts +5 -5
  93. package/dist/internal/passkeys.mjs +1 -1
  94. package/dist/internal/password-credentials.d.mts +11 -0
  95. package/dist/internal/password-credentials.mjs +121 -0
  96. package/dist/internal/password-credentials.mjs.map +1 -0
  97. package/dist/internal/password-kernel.d.mts +15 -28
  98. package/dist/internal/password-kernel.mjs +35 -236
  99. package/dist/internal/password-kernel.mjs.map +1 -1
  100. package/dist/internal/persistence.d.mts +2 -0
  101. package/dist/internal/persistence.mjs +5 -5
  102. package/dist/internal/persistence.mjs.map +1 -1
  103. package/dist/internal/query-operations.d.mts +2 -0
  104. package/dist/internal/session-kernel.d.mts +5 -3
  105. package/dist/internal/session-kernel.mjs +95 -29
  106. package/dist/internal/session-kernel.mjs.map +1 -1
  107. package/dist/internal/sql-oauth-connected-model.d.mts +29 -0
  108. package/dist/internal/sql-oauth-connected-model.mjs +7 -0
  109. package/dist/internal/sql-oauth-connected-model.mjs.map +1 -0
  110. package/dist/internal/sql-oauth-model.d.mts +60 -0
  111. package/dist/internal/sql-oauth-model.mjs +10 -0
  112. package/dist/internal/sql-oauth-model.mjs.map +1 -0
  113. package/dist/internal/sql-persistence.mjs +3 -1
  114. package/dist/internal/sql-persistence.mjs.map +1 -1
  115. package/dist/internal/sql-query.d.mts +12 -0
  116. package/dist/internal/sql-query.mjs +15 -18
  117. package/dist/internal/sql-query.mjs.map +1 -1
  118. package/dist/internal/sql-storage-validation.mjs +54 -0
  119. package/dist/internal/sql-storage-validation.mjs.map +1 -0
  120. package/dist/internal/storage-mapping.mjs +2 -67
  121. package/dist/internal/storage-mapping.mjs.map +1 -1
  122. package/dist/internal/storage-plans.d.mts +7 -0
  123. package/dist/internal/storage-plans.mjs +189 -0
  124. package/dist/internal/storage-plans.mjs.map +1 -0
  125. package/dist/internal/storage-tables.d.mts +0 -99
  126. package/dist/internal/storage-tables.mjs +0 -43
  127. package/dist/internal/storage-tables.mjs.map +1 -1
  128. package/dist/internal/storage-validation.d.mts +6 -1
  129. package/dist/internal/storage-validation.mjs +42 -1
  130. package/dist/internal/storage-validation.mjs.map +1 -1
  131. package/dist/internal/transaction-execution-kernel.d.mts +1 -1
  132. package/dist/internal/transaction-kernel.d.mts +1 -1
  133. package/dist/internal/transaction-kernel.mjs +3 -2
  134. package/dist/internal/transaction-kernel.mjs.map +1 -1
  135. package/package.json +1 -1
  136. package/src/Adapter.ts +97 -8
  137. package/src/OAuthPersistence.ts +427 -0
  138. package/src/index.ts +3 -0
  139. package/src/internal/authority-snapshot.ts +91 -0
  140. package/src/internal/configuration.ts +1 -8
  141. package/src/internal/models/oauth-connected-model.ts +396 -0
  142. package/src/internal/models/oauth-model.ts +708 -0
  143. package/src/internal/models/password-model.ts +0 -120
  144. package/src/internal/native-sql-table.ts +178 -0
  145. package/src/internal/oauth/accounts.ts +838 -0
  146. package/src/internal/oauth/connected-access.ts +388 -0
  147. package/src/internal/oauth/connected-collection.ts +345 -0
  148. package/src/internal/oauth/connected-custody.ts +308 -0
  149. package/src/internal/oauth/connected-flow.ts +562 -0
  150. package/src/internal/oauth/connected-input.ts +147 -0
  151. package/src/internal/oauth/connected-keys.ts +31 -0
  152. package/src/internal/oauth/connected-maintenance.ts +379 -0
  153. package/src/internal/oauth/connected-management.ts +391 -0
  154. package/src/internal/oauth/connected-reference.ts +135 -0
  155. package/src/internal/oauth/connected-settlement.ts +278 -0
  156. package/src/internal/oauth/connected-sign-in.ts +472 -0
  157. package/src/internal/oauth/connected-state.ts +924 -0
  158. package/src/internal/oauth/connected-target.ts +307 -0
  159. package/src/internal/oauth/flow.ts +505 -0
  160. package/src/internal/oauth/input.ts +201 -0
  161. package/src/internal/oauth/owner.ts +22 -0
  162. package/src/internal/oauth/query-compiler.ts +15 -0
  163. package/src/internal/oauth/registration.ts +676 -0
  164. package/src/internal/oauth/state.ts +218 -0
  165. package/src/internal/oauth/target.ts +803 -0
  166. package/src/internal/oauth-kernel.ts +294 -0
  167. package/src/internal/oauth-proxy.ts +176 -0
  168. package/src/internal/password-credentials.ts +245 -0
  169. package/src/internal/password-kernel.ts +98 -712
  170. package/src/internal/persistence.ts +5 -10
  171. package/src/internal/query-operations.ts +2 -0
  172. package/src/internal/session-kernel.ts +231 -81
  173. package/src/internal/sql-oauth-connected-model.ts +145 -0
  174. package/src/internal/sql-oauth-model.ts +263 -0
  175. package/src/internal/sql-persistence.ts +2 -0
  176. package/src/internal/sql-query.ts +16 -13
  177. package/src/internal/sql-storage-validation.ts +64 -0
  178. package/src/internal/storage-mapping.ts +0 -66
  179. package/src/internal/storage-plans.ts +218 -0
  180. package/src/internal/storage-tables.ts +0 -35
  181. package/src/internal/storage-validation.ts +58 -0
  182. package/src/internal/transaction-kernel.ts +6 -2
@@ -0,0 +1,218 @@
1
+ import { Predicate, Schema } from "effect";
2
+
3
+ // These are mapping contracts, not a parser for arbitrary constraint declarations.
4
+ // Role aliases are explicit because their documentation names differ from mapping fields.
5
+ const uniquePlans: Readonly<Record<string, readonly [string, ReadonlyArray<string>]>> = {
6
+ "unique(abuseEvent.moduleId,abuseEvent.action,abuseEvent.scopeKind,abuseEvent.scopeKey,abuseEvent.commandId)":
7
+ ["abuseEvent", ["moduleId", "action", "scopeKind", "scopeKey", "commandId"]],
8
+ "unique(admission.moduleId)": ["admission", ["moduleId"]],
9
+ "unique(attempt.moduleId,attempt.attemptId)": ["attempt", ["moduleId", "attemptId"]],
10
+ "unique(authority.subjectId,authority.credentialId)": [
11
+ "authority",
12
+ ["subjectId", "credentialId"],
13
+ ],
14
+ "unique(authorityCredential.subjectId,authorityCredential.credentialId)": [
15
+ "authorityCredential",
16
+ ["subjectId", "credentialId"],
17
+ ],
18
+ "unique(command.moduleId,command.commandId)": ["command", ["moduleId", "commandId"]],
19
+ "unique(connectedAdmission.admissionId)": ["admission", ["admissionId"]],
20
+ "unique(connectedClient.clientKey)": ["client", ["clientKey"]],
21
+ "unique(connectedCohort.cohortKey)": ["cohort", ["cohortKey"]],
22
+ "unique(connectedCommand.moduleId,connectedCommand.commandId)": [
23
+ "command",
24
+ ["moduleId", "commandId"],
25
+ ],
26
+ "unique(connectedFlow.moduleId,connectedFlow.commandId)": ["flow", ["moduleId", "commandId"]],
27
+ "unique(connectedFlow.moduleId,connectedFlow.flowId)": ["flow", ["moduleId", "flowId"]],
28
+ "unique(connectedFlow.stateDigest)": ["flow", ["stateDigest"]],
29
+ "unique(connectedGrant.moduleId,connectedGrant.grantId)": ["grant", ["moduleId", "grantId"]],
30
+ "unique(connectedGrant.moduleId,connectedGrant.subjectId,connectedGrant.profileKey,connectedGrant.activeIdentityKey)":
31
+ ["grant", ["moduleId", "subjectId", "profileKey", "activeIdentityKey"]],
32
+ "unique(connectedRevocation.jobId)": ["job", ["jobId"]],
33
+ "unique(continuation.moduleId,continuation.continuationId)": [
34
+ "continuation",
35
+ ["moduleId", "continuationId"],
36
+ ],
37
+ "unique(continuation.moduleId,continuation.digest)": ["continuation", ["moduleId", "digest"]],
38
+ "unique(credential.credentialId)": ["credential", ["credentialId"]],
39
+ "unique(credential.identityKey)": ["credential", ["identityKey"]],
40
+ "unique(credential.moduleId,credential.credentialId)": [
41
+ "credential",
42
+ ["moduleId", "credentialId"],
43
+ ],
44
+ "unique(credential.moduleId,credential.subjectId)": ["credential", ["moduleId", "subjectId"]],
45
+ "unique(emailCommand.moduleId,emailCommand.commandId)": ["command", ["moduleId", "commandId"]],
46
+ "unique(emailCredential.moduleId,emailCredential.credentialId)": [
47
+ "credential",
48
+ ["moduleId", "credentialId"],
49
+ ],
50
+ "unique(emailCredential.moduleId,emailCredential.identifierNamespace,emailCredential.identifierValue)":
51
+ ["credential", ["moduleId", "identifierNamespace", "identifierValue"]],
52
+ "unique(emailRegistration.moduleId,emailRegistration.commandId)": [
53
+ "registration",
54
+ ["moduleId", "commandId"],
55
+ ],
56
+ "unique(emailRegistration.pendingReference)": ["registration", ["pendingReference"]],
57
+ "unique(failureEvent.moduleId,failureEvent.seriesKey,failureEvent.commandId)": [
58
+ "failureEvent",
59
+ ["moduleId", "seriesKey", "commandId"],
60
+ ],
61
+ "unique(flow.flowId)": ["flow", ["flowId"]],
62
+ "unique(flow.moduleId,flow.commandId)": ["flow", ["moduleId", "commandId"]],
63
+ "unique(flow.moduleId,flow.flowId)": ["flow", ["moduleId", "flowId"]],
64
+ "unique(flow.stateDigest)": ["flow", ["stateDigest"]],
65
+ "unique(generation.moduleId,generation.deliveryId)": ["generation", ["moduleId", "deliveryId"]],
66
+ "unique(generation.moduleId,generation.proofId)": ["generation", ["moduleId", "proofId"]],
67
+ "unique(identifier.namespace,identifier.value)": ["identifier", ["namespace", "value"]],
68
+ "unique(intent.digest)": ["intent", ["digest"]],
69
+ "unique(intent.flowId)": ["intent", ["flowId"]],
70
+ "unique(intent.moduleId,intent.commandId)": ["intent", ["moduleId", "commandId"]],
71
+ "unique(intent.moduleId,intent.flowId)": ["intent", ["moduleId", "flowId"]],
72
+ "unique(intent.moduleId,intent.intentId)": ["intent", ["moduleId", "intentId"]],
73
+ "unique(intent.moduleId,intent.reference)": ["intent", ["moduleId", "reference"]],
74
+ "unique(ownership.identityKey)": ["ownership", ["identityKey"]],
75
+ "unique(pending.digest)": ["pending", ["digest"]],
76
+ "unique(pending.flowId)": ["pending", ["flowId"]],
77
+ "unique(rateScope.moduleId,rateScope.purpose,rateScope.action,rateScope.scopeKind,rateScope.scopeKey)":
78
+ ["rateScope", ["moduleId", "purpose", "action", "scopeKind", "scopeKey"]],
79
+ "unique(registration.moduleId,registration.requestId)": [
80
+ "registration",
81
+ ["moduleId", "requestId"],
82
+ ],
83
+ "unique(registration.recoveryReference)": ["registration", ["recoveryReference"]],
84
+ "unique(registrationCommand.moduleId,registrationCommand.commandId)": [
85
+ "command",
86
+ ["moduleId", "commandId"],
87
+ ],
88
+ "unique(request.moduleId,request.requestId)": ["request", ["moduleId", "requestId"]],
89
+ "unique(series.moduleId,series.purpose,series.scopeKey)": [
90
+ "series",
91
+ ["moduleId", "purpose", "scopeKey"],
92
+ ],
93
+ "unique(session.digest)": ["session", ["digest"]],
94
+ "unique(tombstone.subjectId,tombstone.sessionId)": ["tombstone", ["subjectId", "sessionId"]],
95
+ "unique(tuple.identityKey)": ["ownership.tuple", ["identityKey"]],
96
+ "unique(unlinkCommand.moduleId,unlinkCommand.commandId)": ["command", ["moduleId", "commandId"]],
97
+ };
98
+
99
+ const arrayRoles: Readonly<Record<string, string>> = {
100
+ credentialId: "credential",
101
+ factor: "authority",
102
+ credentialOwnership: "credentialOwnership",
103
+ handleOwnership: "handleOwnership",
104
+ boundSubjectHandle: "handleOwnership",
105
+ flow: "flow",
106
+ command: "flow",
107
+ admission: "admission",
108
+ charge: "charge",
109
+ intentFlow: "intent",
110
+ intentCommand: "intent",
111
+ handle: "handle",
112
+ };
113
+
114
+ const nestedMappings = [
115
+ "read",
116
+ "signIn",
117
+ "password",
118
+ "proof",
119
+ "proofs",
120
+ "revocation",
121
+ "pending",
122
+ "session",
123
+ "source",
124
+ ];
125
+
126
+ const constraintGroups = [
127
+ "constraints",
128
+ "tupleConstraints",
129
+ "registrationConstraints",
130
+ "managementConstraints",
131
+ ];
132
+
133
+ const MappingRecord = Schema.Record(Schema.String, Schema.Unknown);
134
+ const record = Schema.decodeUnknownSync(MappingRecord);
135
+
136
+ export const storageKeyPlans = (
137
+ input: unknown,
138
+ columnsFor: (table: object) => Readonly<Record<string, unknown>>,
139
+ ) => {
140
+ const mapping = record(input);
141
+ const result: Array<{ readonly table: object; readonly keys: ReadonlyArray<string> }> = [];
142
+
143
+ for (const group of constraintGroups) {
144
+ if (mapping[group] === undefined) continue;
145
+ for (const [name, declaration] of Object.entries(record(mapping[group]))) {
146
+ if (declaration === "notNull(provisioningRequest.subjectId)") continue;
147
+ let role: string;
148
+ let keys: ReadonlyArray<string>;
149
+
150
+ if (typeof declaration === "string") {
151
+ const plan = uniquePlans[declaration];
152
+
153
+ if (plan !== undefined) [role, keys] = plan;
154
+ else if (declaration === "unique(id)") {
155
+ role = "subject";
156
+ keys = ["id"];
157
+ } else if (declaration === "unique(scope)") {
158
+ role = name;
159
+ keys = ["scope"];
160
+ } else if (declaration === "unique(credentialId)") {
161
+ role = "credential";
162
+ keys = ["id"];
163
+ } else if (declaration === "unique(namespace,value)") {
164
+ role = "identifier";
165
+ keys = ["namespace", "value"];
166
+ } else if (declaration === "unique(provider,issuer,subject)") {
167
+ role = "externalIdentity";
168
+ keys = ["provider", "issuer", "subject"];
169
+ } else if (declaration === "unique(requestId)") {
170
+ role = "provisioningRequest";
171
+ keys = ["requestId"];
172
+ } else throw new Error("Unknown storage constraint");
173
+ } else if (Array.isArray(declaration) && declaration.every(Predicate.isString)) {
174
+ const mappedRole =
175
+ group === "managementConstraints" && name === "command" ? "command" : arrayRoles[name];
176
+
177
+ if (mappedRole === undefined) throw new Error("Unknown storage constraint");
178
+ role = mappedRole;
179
+ keys = declaration;
180
+ } else throw new Error("Invalid storage constraint");
181
+
182
+ let target: unknown = mapping;
183
+
184
+ for (const segment of role.split(".")) target = record(target)[segment];
185
+ // Session authority places its pending tables in one nested mapping.
186
+ if (target === undefined && Schema.is(MappingRecord)(mapping.pending))
187
+ target = mapping.pending[role];
188
+ let mapped = record(target);
189
+
190
+ if (role === "pending" && mapped.table === undefined) mapped = record(mapped.pending);
191
+ // OAuth ownership can be integrated into its tuple authority or separate.
192
+ if (role === "ownership" && mapped.table === undefined)
193
+ mapped = record(mapped.external ?? mapped.tuple);
194
+ if (!Predicate.isObject(mapped.table)) throw new Error("Missing storage table");
195
+ const columns = columnsFor(mapped.table);
196
+
197
+ result.push({
198
+ table: mapped.table,
199
+ keys: keys.map((key) => {
200
+ const value = mapped[key];
201
+
202
+ if (typeof value !== "string" || columns[value] === undefined)
203
+ throw new Error("Missing mapped key");
204
+
205
+ return value;
206
+ }),
207
+ });
208
+ }
209
+ }
210
+ for (const nested of nestedMappings) {
211
+ const value = mapping[nested];
212
+
213
+ if (Schema.is(MappingRecord)(value) && value.table === undefined)
214
+ result.push(...storageKeyPlans(value, columnsFor));
215
+ }
216
+
217
+ return result;
218
+ };
@@ -58,41 +58,6 @@ export const storageTables = {
58
58
  ["moduleId", "credentialId"],
59
59
  ],
60
60
  ),
61
- passwordAttempts: spec(
62
- {
63
- moduleId: text,
64
- action: text,
65
- attemptId: text,
66
- identifierNamespace: text,
67
- identifierValue: text,
68
- subjectId: optionalText,
69
- credentialId: optionalText,
70
- securityRevision: optionalText,
71
- credentialRevision: optionalText,
72
- verifierVersion: optionalText,
73
- identifierBindingRevision: optionalText,
74
- admittedAt: integer,
75
- deadline: integer,
76
- retentionUntil: integer,
77
- state: text,
78
- },
79
- [["moduleId", "attemptId"]],
80
- ),
81
- passwordScopes: spec({ moduleId: text, action: text, scopeKind: text, scopeKey: text }, [
82
- ["moduleId", "action", "scopeKind", "scopeKey"],
83
- ]),
84
- passwordCharges: spec(
85
- {
86
- moduleId: text,
87
- action: text,
88
- scopeKind: text,
89
- scopeKey: text,
90
- attemptId: text,
91
- occurredAt: integer,
92
- retentionUntil: integer,
93
- },
94
- [["moduleId", "action", "scopeKind", "scopeKey", "attemptId"]],
95
- ),
96
61
  passwordCommands: spec(
97
62
  {
98
63
  moduleId: text,
@@ -3,6 +3,7 @@ import { SqlClient } from "effect/sql/SqlClient";
3
3
 
4
4
  import { PersistenceConfigurationError } from "./configuration";
5
5
  import type { StorageTable } from "./storage-tables";
6
+ import { NativeDatabase } from "./transaction-kernel";
6
7
 
7
8
  const Columns = Schema.Array(Schema.String);
8
9
  const Keys = Schema.Array(Columns);
@@ -40,6 +41,63 @@ export interface PhysicalStorageTable {
40
41
  export const qualifiedTableName = (table: Pick<StorageTable, "name" | "schema">) =>
41
42
  (table.schema === undefined ? "" : quote(table.schema) + ".") + quote(table.name);
42
43
 
44
+ export interface PhysicalTextColumn {
45
+ readonly table: Pick<PhysicalStorageTable, "name" | "schema">;
46
+ readonly name: string;
47
+ }
48
+
49
+ const TextColumns = Schema.Array(Schema.Struct({ type: Schema.String, collation: Schema.String }));
50
+
51
+ /** One acquisition-only read: presence/unique-key validation does not establish
52
+ * equality compatibility. SQLite's pragma omits column collations, so only
53
+ * ordinary TEXT tables with no COLLATE declaration qualify (default BINARY). */
54
+ export const canJoinTextColumns = Effect.fnUntraced(function* (
55
+ left: PhysicalTextColumn | undefined,
56
+ right: PhysicalTextColumn | undefined,
57
+ ) {
58
+ if (left === undefined || right === undefined) return false;
59
+ const { $client } = yield* NativeDatabase;
60
+ const client = $client.withoutTransforms();
61
+ const columns = [left, right];
62
+
63
+ if (
64
+ client.onDialectOrElse({ sqlite: () => true, orElse: () => false }) &&
65
+ columns.some((c) => c.table.schema !== undefined && c.table.schema !== "main")
66
+ )
67
+ return false;
68
+
69
+ const query = client.onDialectOrElse({
70
+ pg: () => client`
71
+ select a.atttypid::text as type, a.attcollation::text as collation
72
+ from (values ${client.join(", ", false)(columns.map((c) => client`(${qualifiedTableName(c.table)}::text, ${c.name}::text)`))}) requested(relation, name)
73
+ join pg_attribute a on a.attrelid = to_regclass(requested.relation)
74
+ and a.attname = requested.name and a.attnum > 0 and not a.attisdropped
75
+ where a.atttypid in ('text'::regtype, 'varchar'::regtype)`,
76
+ sqlite: () => client`
77
+ with requested(relation, name) as (values ${client.join(", ", false)(columns.map((c) => client`(${c.table.name}, ${c.name})`))})
78
+ select 'text' as type, 'BINARY' as collation from requested
79
+ join sqlite_schema s on s.name = requested.relation and s.type = 'table'
80
+ join pragma_table_info(requested.relation) c on c.name = requested.name
81
+ where upper(c.type) = 'TEXT' and instr(lower(s.sql), 'collate') = 0`,
82
+ mysql: () => client`
83
+ select c.data_type as type, c.collation_name as collation
84
+ from (${client.join(" union all ", false)(columns.map((c) => client`select ${c.table.schema ?? null} as schema_name, ${c.table.name} as table_name, ${c.name} as column_name`))}) requested
85
+ join information_schema.columns c
86
+ on c.table_schema = coalesce(requested.schema_name, database())
87
+ and c.table_name = requested.table_name and c.column_name = requested.column_name
88
+ where c.data_type in ('varchar', 'text', 'tinytext', 'mediumtext', 'longtext')`,
89
+ orElse: () => Effect.succeed([]),
90
+ });
91
+
92
+ const rows = yield* query.pipe(Effect.flatMap(Schema.decodeUnknownEffect(TextColumns)));
93
+
94
+ return (
95
+ rows.length === 2 &&
96
+ rows[0]?.type === rows[1]?.type &&
97
+ rows[0]?.collation === rows[1]?.collation
98
+ );
99
+ });
100
+
43
101
  const validateKeys = Effect.fnUntraced(function* (
44
102
  table: PhysicalStorageTable,
45
103
  required: ReadonlyArray<ReadonlyArray<string>>,
@@ -366,9 +366,13 @@ export const makeTransactionKernel = <
366
366
  ? toStatement(query)
367
367
  : (query as NativeQuery<Row[]>);
368
368
 
369
- invariant(rows.length === 1);
369
+ const row = rows[0];
370
370
 
371
- return rows[0]!.value === 1;
371
+ invariant(rows.length === 1 && row !== undefined);
372
+
373
+ // SQLite executes the raw statement, before logical field decoding.
374
+ // This query projects one value; its alias belongs to the adapter compiler.
375
+ return Object.values(row)[0] === 1;
372
376
  });
373
377
 
374
378
  const guardedQuery = (table: Table, where: SQL, values: Row, guard: GuardedUpdate) => {