@drhomet/mithaq 0.0.0-stage → 0.1.1

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 (151) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/payload.txt +22 -0
  3. package/LICENSES/ponytail.txt +21 -0
  4. package/dist/asas/actor.d.ts +96 -0
  5. package/dist/asas/actor.js +114 -0
  6. package/dist/asas/build.d.ts +231 -0
  7. package/dist/asas/build.js +787 -0
  8. package/dist/asas/cli/mithaq.d.ts +2 -0
  9. package/dist/asas/cli/mithaq.js +145 -0
  10. package/dist/asas/cli/upgrade.d.ts +20 -0
  11. package/dist/asas/cli/upgrade.js +147 -0
  12. package/dist/asas/http/fetch.d.ts +23 -0
  13. package/dist/asas/http/fetch.js +115 -0
  14. package/dist/asas/http/orpc.d.ts +43 -0
  15. package/dist/asas/http/orpc.js +135 -0
  16. package/dist/asas/runner/bulk.d.ts +20 -0
  17. package/dist/asas/runner/bulk.js +191 -0
  18. package/dist/asas/runner/errors.d.ts +60 -0
  19. package/dist/asas/runner/errors.js +59 -0
  20. package/dist/asas/runner/read.d.ts +68 -0
  21. package/dist/asas/runner/read.js +579 -0
  22. package/dist/asas/runner/runtime.d.ts +21 -0
  23. package/dist/asas/runner/runtime.js +8 -0
  24. package/dist/asas/runner/steps.d.ts +400 -0
  25. package/dist/asas/runner/steps.js +271 -0
  26. package/dist/asas/runner/tenant.d.ts +54 -0
  27. package/dist/asas/runner/tenant.js +213 -0
  28. package/dist/asas/runner/write.d.ts +444 -0
  29. package/dist/asas/runner/write.js +791 -0
  30. package/dist/auth/better-auth/access.d.ts +15 -0
  31. package/dist/auth/better-auth/access.js +71 -0
  32. package/dist/auth/better-auth/generate.d.ts +29 -0
  33. package/dist/auth/better-auth/generate.js +310 -0
  34. package/dist/auth/better-auth/index.d.ts +92 -0
  35. package/dist/auth/better-auth/index.js +81 -0
  36. package/dist/auth/better-auth/tenant.d.ts +18 -0
  37. package/dist/auth/better-auth/tenant.js +26 -0
  38. package/dist/auth/local/index.d.ts +285 -0
  39. package/dist/auth/local/index.js +736 -0
  40. package/dist/base/decimal.d.ts +9 -0
  41. package/dist/base/decimal.js +16 -0
  42. package/dist/base/fields.d.ts +123 -0
  43. package/dist/base/fields.js +51 -0
  44. package/dist/base/index.d.ts +46 -0
  45. package/dist/base/index.js +5 -0
  46. package/dist/base/kinds.d.ts +54 -0
  47. package/dist/base/kinds.js +78 -0
  48. package/dist/base/rules.d.ts +73 -0
  49. package/dist/base/rules.js +80 -0
  50. package/dist/drizzle/index.d.ts +438 -0
  51. package/dist/drizzle/index.js +832 -0
  52. package/dist/drizzle/kinds.d.ts +27 -0
  53. package/dist/drizzle/kinds.js +41 -0
  54. package/dist/drizzle/migrate.d.ts +133 -0
  55. package/dist/drizzle/migrate.js +1285 -0
  56. package/dist/drizzle/proxy/index.d.ts +1 -0
  57. package/dist/drizzle/proxy/index.js +1 -0
  58. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  59. package/dist/drizzle/proxy/node-postgres.js +1 -0
  60. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  61. package/dist/drizzle/proxy/pg-core.js +1 -0
  62. package/dist/drizzle/rules.d.ts +336 -0
  63. package/dist/drizzle/rules.js +44 -0
  64. package/dist/index.d.ts +45 -0
  65. package/dist/index.js +36 -0
  66. package/dist/mithaq/define.d.ts +288 -0
  67. package/dist/mithaq/define.js +43 -0
  68. package/dist/mithaq/generate-messages.d.ts +38 -0
  69. package/dist/mithaq/generate-messages.js +234 -0
  70. package/dist/mithaq/language.d.ts +335 -0
  71. package/dist/mithaq/language.js +76 -0
  72. package/dist/mithaq/lock.d.ts +71 -0
  73. package/dist/mithaq/lock.js +329 -0
  74. package/dist/mithaq/messages.d.ts +55 -0
  75. package/dist/mithaq/messages.js +106 -0
  76. package/dist/mithaq/refusal.d.ts +6 -0
  77. package/dist/mithaq/refusal.js +6 -0
  78. package/dist/mithaq/resolve.d.ts +10 -0
  79. package/dist/mithaq/resolve.js +954 -0
  80. package/dist/mithaq/rows.d.ts +87 -0
  81. package/dist/mithaq/rows.js +174 -0
  82. package/dist/mithaq/rukn.d.ts +72 -0
  83. package/dist/mithaq/rukn.js +15 -0
  84. package/dist/mithaq/standard-schema.d.ts +38 -0
  85. package/dist/mithaq/standard-schema.js +2 -0
  86. package/dist/mithaq/types.d.ts +358 -0
  87. package/dist/mithaq/types.js +15 -0
  88. package/dist/mithaq/user.d.ts +19 -0
  89. package/dist/mithaq/user.js +16 -0
  90. package/dist/mithaq/where.d.ts +32 -0
  91. package/dist/mithaq/where.js +87 -0
  92. package/dist/rbac/index.d.ts +105 -0
  93. package/dist/rbac/index.js +182 -0
  94. package/dist/rbac/steps.d.ts +22 -0
  95. package/dist/rbac/steps.js +161 -0
  96. package/dist/rich-text/index.d.ts +27 -0
  97. package/dist/rich-text/index.js +58 -0
  98. package/dist/rich-text/rich-text.d.ts +159 -0
  99. package/dist/rich-text/rich-text.js +372 -0
  100. package/dist/screen/client.d.ts +153 -0
  101. package/dist/screen/client.js +68 -0
  102. package/dist/screen/generate.d.ts +14 -0
  103. package/dist/screen/generate.js +226 -0
  104. package/dist/screen/index.d.ts +479 -0
  105. package/dist/screen/index.js +645 -0
  106. package/dist/screen/react.d.ts +95 -0
  107. package/dist/screen/react.js +58 -0
  108. package/dist/screen/studio.d.ts +24 -0
  109. package/dist/screen/studio.js +68 -0
  110. package/dist/service/index.d.ts +63 -0
  111. package/dist/service/index.js +15 -0
  112. package/dist/storage/allowed-type.d.ts +5 -0
  113. package/dist/storage/allowed-type.js +7 -0
  114. package/dist/storage/index.d.ts +43 -0
  115. package/dist/storage/index.js +44 -0
  116. package/dist/storage/storage.d.ts +384 -0
  117. package/dist/storage/storage.js +405 -0
  118. package/dist/versions/index.d.ts +37 -0
  119. package/dist/versions/index.js +8 -0
  120. package/dist/versions/versions.d.ts +104 -0
  121. package/dist/versions/versions.js +160 -0
  122. package/dist/zod/index.d.ts +184 -0
  123. package/dist/zod/index.js +121 -0
  124. package/dist/zod/kinds.d.ts +34 -0
  125. package/dist/zod/kinds.js +49 -0
  126. package/dist/zod/rules.d.ts +117 -0
  127. package/dist/zod/rules.js +67 -0
  128. package/package.json +63 -4
  129. package/r/mithaq-cells.json +20 -0
  130. package/r/mithaq-controls.json +37 -0
  131. package/r/mithaq-directional-icon.json +15 -0
  132. package/r/mithaq-expected-updated-at.json +15 -0
  133. package/r/mithaq-field-shell.json +18 -0
  134. package/r/mithaq-filter-operators.json +15 -0
  135. package/r/mithaq-form.json +33 -0
  136. package/r/mithaq-list-columns.json +23 -0
  137. package/r/mithaq-list-filters.json +24 -0
  138. package/r/mithaq-list-pagination.json +24 -0
  139. package/r/mithaq-list-query.json +18 -0
  140. package/r/mithaq-list-selection.json +19 -0
  141. package/r/mithaq-list.json +33 -0
  142. package/r/mithaq-nav.json +24 -0
  143. package/r/mithaq-nodes.json +34 -0
  144. package/r/mithaq-relationship-cell.json +20 -0
  145. package/r/mithaq-relationship-control.json +20 -0
  146. package/r/mithaq-rich-text.json +40 -0
  147. package/r/mithaq-screens-host.json +96 -0
  148. package/r/mithaq-storage.json +44 -0
  149. package/r/mithaq-upload.json +29 -0
  150. package/r/registry.json +550 -0
  151. package/README.md +0 -3
@@ -0,0 +1,787 @@
1
+ import { rowsOf } from '../mithaq/rows.js';
2
+ import { accessReport, actor, actorPermissions, isAuthDeclaration, markAdminAnswer } from './actor.js';
3
+ import { betterAuthStrategy } from '../auth/better-auth/index.js';
4
+ import { holdsVerificationSteps, isLocalAuthTarget, localAuthStrategy } from '../auth/local/index.js';
5
+ import { bulkSeals, outputOf, sealOf, storeSeals } from './runner/steps.js';
6
+ import { running } from './runner/runtime.js';
7
+ import { tenancy } from './runner/tenant.js';
8
+ import { assertWhere, defaultAccess, defaultAuthorize, projectReadableFields } from './runner/write.js';
9
+ import { combineWhere } from '../mithaq/where.js';
10
+ import { bulk, read as readers, write } from '../service/index.js';
11
+ import { invalid } from './runner/errors.js';
12
+ import { fieldlessOperation, keepOperationKeys } from '../rbac/steps.js';
13
+ import { resolvedKayans } from '../mithaq/types.js';
14
+ import { accessTargetOf, isAccess, isScreen, isService, isStorage, isStore, isTenant, isVersions, serviceKeys, serviceTargetOf } from '../mithaq/language.js';
15
+ import { screensOf } from '../screen/index.js';
16
+ import { messagesOf } from '../mithaq/messages.js';
17
+ import { resolve } from '../mithaq/resolve.js';
18
+ import { recordStorage, resolveStorage, storageRoutes } from '../storage/storage.js';
19
+ // A symbol: no schema of something a browser can send takes it, so a schema that names
20
+ // the key answers with an issue at the key.
21
+ const probe = Symbol('mithaq.operation.input');
22
+ /** The service target's four numbers, each a positive integer the project names. */
23
+ const pageKeys = ['defaultLimit', 'maxLimit', 'joinLimit', 'bulkLimit'];
24
+ // An input field the schema names no key for would be dropped or refused at every click,
25
+ // so it fails the build. Asked through Standard Schema alone: a probe value at that key.
26
+ const refuseUndeclaredInput = (where, screens, schema) => {
27
+ const names = new Set(Object.values(screens ?? {}).flatMap((screen) => Object.keys(screen?.fields ?? {})));
28
+ for (const name of names) {
29
+ const answer = schema['~standard'].validate({ [name]: probe });
30
+ if (answer instanceof Promise) {
31
+ // A rejection nobody reads would kill the process after `buildAsas` returned.
32
+ answer.catch(() => { });
33
+ throw new Error(`${where} declares the button field "${name}", and this operation's own schema validates asynchronously, which a synchronous build cannot await, so whether the schema names that key cannot be read. Give this operation a synchronous schema, or drop the field.`);
34
+ }
35
+ // A path segment is a key or an object carrying one.
36
+ const named = answer.issues?.some((issue) => {
37
+ const segment = issue.path?.[0];
38
+ return segment !== undefined && String(typeof segment === 'object' ? segment.key : segment) === name;
39
+ });
40
+ if (named)
41
+ continue;
42
+ throw new Error(`${where} declares the button field "${name}", and this operation's own schema names no key of that name, so the dialog would draw a box over a value the parse drops. Name it in the operation's validate function, or drop the field.`);
43
+ }
44
+ };
45
+ // Every operation takes its input, which is exactly what a client may send, then the
46
+ // options, which are the server's alone.
47
+ const operations = (kayan, store, schema,
48
+ // A global's update publishes this; the runner never parses it.
49
+ singleUpdate, cap, read, kayans, of, audit, admin, authorize, catalog,
50
+ /** True where the runner declares a context, which a served call must then carry. */
51
+ contextDeclared,
52
+ /** The most rows a served read of many may ask for, and one bulk write may match. */
53
+ { maxLimit, bulkLimit }, tenant) => {
54
+ // Bound, so a step cannot reach `transaction` or the adapter's own client, and sealed, so no
55
+ // step writes a tenant-owned row outside the active organization.
56
+ const writes = tenant.seal({
57
+ insert: store.insert.bind(store),
58
+ remove: store.remove.bind(store),
59
+ update: store.update.bind(store),
60
+ });
61
+ const table = [];
62
+ /** The bulk lists, held for the second pass: each needs a method the first builds. */
63
+ const bulkLists = [];
64
+ // Options come from the plug alone, so nothing a client sends reaches a step as one.
65
+ // On a runner that declares a context, a plug carrying none is refused before any step.
66
+ const publish = (name, effect, input, method, bounded = false) => {
67
+ // The declared arity is what this widens across, which TypeScript asks two steps for.
68
+ const called = method;
69
+ table.push({
70
+ kayan: kayan.name,
71
+ name,
72
+ effect,
73
+ single: !!kayan.single,
74
+ input,
75
+ call: (given, { actor, context, draft, autosave, tenant }) => {
76
+ if (contextDeclared && !context) {
77
+ throw new Error(`Kayan "${kayan.name}" operation "${name}" was served with no context, and this build's runner declares one. Give every http plug a context function: fetchHttp({ context }) or orpcHttp({ context }).`);
78
+ }
79
+ if (bounded && given && typeof given === 'object' && 'limit' in given) {
80
+ const limit = given.limit;
81
+ if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > maxLimit) {
82
+ throw invalid({ message: `HTTP read option "limit" must be an integer from 1 to ${maxLimit}.`, key: 'error.invalid.read_option_range', params: { option: 'limit', minimum: 1, maximum: maxLimit } });
83
+ }
84
+ }
85
+ return called(given, { actor, ...(context ? { context } : {}), ...(draft ? { draft } : {}), ...(autosave ? { autosave } : {}), ...(tenant !== undefined ? { tenant } : {}) });
86
+ },
87
+ });
88
+ return method;
89
+ };
90
+ const methods = Object.create(null);
91
+ for (const [operation, declaration] of Object.entries(kayan.operations)) {
92
+ const declared = declaration.input;
93
+ // Resolved once, so every later reader sees a schema.
94
+ const publicOutput = outputOf(declaration)?.(schema) ?? declaration.output;
95
+ // A list that declares no shape takes the one its own work implies, which the schema target derives.
96
+ if (declaration.effect === 'read') {
97
+ // A version is addressed by its own id, and a list of them takes the page query.
98
+ const shape = declared?.(schema) ?? (declaration.mode === 'one' ? schema.byID : declaration.mode === 'version' ? schema.id : schema.list);
99
+ refuseUndeclaredInput(`Kayan "${kayan.name}" operation "${operation}"`, declaration.targets, shape);
100
+ const run = read.operation({ name: operation, declaration: { ...declaration, output: publicOutput }, input: shape, output: schema.row, draftOutput: schema.draft });
101
+ methods[operation] = publish(operation, declaration.effect, shape, run, declaration.mode === 'many' || declaration.mode === 'versions');
102
+ continue;
103
+ }
104
+ const list = declaration.steps;
105
+ // A restore is an update of the row its version belongs to, addressed by the version's own id.
106
+ const acting = list
107
+ .map(sealOf)
108
+ .find((held) => held && (storeSeals.includes(held) || bulkSeals.includes(held) || held === 'restoreVersion'));
109
+ const kind = (acting === 'restoreVersion' ? 'update' : acting);
110
+ const shape = declared?.(schema)
111
+ ?? (bulkSeals.includes(kind)
112
+ // The list shape constrains no value: the runner checks the `where`, and each row's
113
+ // single-row write checks `data`.
114
+ ? schema.list
115
+ : kind === 'insert' ? schema.create : kind === 'remove' ? schema.id : schema.update);
116
+ // The runner writes the found row's id onto the data, so a caller sends none. Derived
117
+ // by the schema target, since `.partial()` is a zod method another target lacks.
118
+ refuseUndeclaredInput(`Kayan "${kayan.name}" operation "${operation}"`, declaration.targets, shape);
119
+ const input = acting === 'restoreVersion' && !declared
120
+ ? schema.id
121
+ : !kayan.single || kind !== 'update' || declared ? shape : singleUpdate ?? shape;
122
+ const tools = {
123
+ operation,
124
+ effect: declaration.effect,
125
+ kind,
126
+ // A restore's own input is written over the id, so it is handed the update shape in the id's place.
127
+ schema: acting === 'restoreVersion' && declared ? declared({ ...schema, id: schema.update }) : shape,
128
+ // A global's update makes the row when there is none, so that branch parses the strict create
129
+ // shape; a first update short of a required field passes the boundary and is refused here.
130
+ ...((kayan.single || kayan.versions?.drafts) && kind === 'update' ? { insertSchema: schema.create } : {}),
131
+ ...(schema.draft ? { draftSchema: schema.draft, draftWrite: schema.draftWrite ?? schema.draft } : {}),
132
+ // A draft save through an operation's own input keeps that input's keys and checks kinds alone.
133
+ ...(schema.draft && declared && (kind === 'insert' || operation === 'update' || acting === 'restoreVersion')
134
+ ? { draftInput: declared({ ...schema, create: schema.draftWrite ?? schema.draft, update: schema.draftWrite ?? schema.draft, ...(acting === 'restoreVersion' && { id: schema.draftWrite ?? schema.draft }) }) }
135
+ : {}),
136
+ output: schema.row,
137
+ publicOutput,
138
+ store: writes,
139
+ audit,
140
+ catalog,
141
+ of: of(declaration.effect),
142
+ // Checked rather than asserted, so dropping a member fails to compile; the assertion carries `kayans` alone.
143
+ };
144
+ Object.defineProperty(tools, 'kayans', {
145
+ enumerable: true,
146
+ get: () => kayans(declaration.effect),
147
+ });
148
+ // Built in the second pass, where every single-row write it delegates to exists.
149
+ if (bulkSeals.includes(kind)) {
150
+ bulkLists.push({ operation, declaration, tools, input });
151
+ continue;
152
+ }
153
+ const run = write({ kayan, declaration, tools, store, cap, authorize, admin, tenantOf: tenant.active });
154
+ methods[operation] = publish(operation, declaration.effect, input, run);
155
+ }
156
+ for (const { operation, declaration, tools, input } of bulkLists) {
157
+ const single = declaration.effect === 'update' ? 'update' : 'delete';
158
+ const run = bulk({
159
+ kayan,
160
+ declaration,
161
+ tools,
162
+ store,
163
+ single: methods[single],
164
+ cap,
165
+ bulkLimit,
166
+ authorize,
167
+ admin,
168
+ tenantOf: tenant.active,
169
+ });
170
+ methods[operation] = publish(operation, declaration.effect, input, run);
171
+ }
172
+ return { methods, table: table };
173
+ };
174
+ // `defineMithaq` merges without refusing, so the clashes are refused here, naming both sides.
175
+ const refuseArkan = (language) => {
176
+ const arkan = language.arkan ?? [];
177
+ for (const [key, noun] of [['kinds', 'kind'], ['rules', 'rule'], ['targets', 'target']]) {
178
+ const by = new Map();
179
+ for (const rukn of arkan) {
180
+ for (const name of Object.keys(rukn[key] ?? {})) {
181
+ const other = by.get(name);
182
+ if (other !== undefined) {
183
+ throw new Error(`Rukn "${other}" and rukn "${rukn.name}" both bring ${noun} "${name}". One ${noun} is brought by one rukn: drop it from one of them.`);
184
+ }
185
+ by.set(name, rukn.name);
186
+ }
187
+ }
188
+ }
189
+ for (const rukn of arkan) {
190
+ for (const [name, target] of Object.entries(rukn.targets ?? {})) {
191
+ if (language.targets[name] !== target) {
192
+ throw new Error(`The project lays down target "${name}", and rukn "${rukn.name}" builds one under that key. Rename the project's target or leave it to the rukn.`);
193
+ }
194
+ }
195
+ for (const capability of rukn.needs ?? []) {
196
+ if (!Object.values(language.targets).some((target) => target.capability === capability)) {
197
+ throw new Error(`Rukn "${rukn.name}" needs a target of capability "${capability}", and this build lays down none. Add one to targets.`);
198
+ }
199
+ }
200
+ }
201
+ };
202
+ export const buildAsas = (language, { kayan, global, auth, http, messages, statements: declaredStatements, }) => {
203
+ // Refused before any work: core picks no plug of its own.
204
+ if (!http?.length) {
205
+ throw new Error('buildAsas was handed no "http", and core picks no plug of its own. Name the plugs: http: [fetchHttp()], or http: [orpcHttp(), fetchHttp()] for the typed client.');
206
+ }
207
+ const globals = (global ?? []);
208
+ const globalNames = new Set(globals.map((one) => one.name));
209
+ // One list, so a name declared twice is refused whichever list it is in.
210
+ const declared = [...kayan, ...globals];
211
+ refuseArkan(language);
212
+ // A kayan writing `targets.screen` would read as every screen and mean one.
213
+ if (Object.hasOwn(language.targets, 'screen')) {
214
+ throw new Error('A target is named "screen", which is the word for the capability. Name a screen for what it draws, such as "dashboard" or "portal".');
215
+ }
216
+ // A build with no runner publishes nothing, so none is refused. Asked before `resolve`,
217
+ // which reads every key off the target.
218
+ const noOperations = (where, holder) => {
219
+ if (!Object.hasOwn(holder, 'operations'))
220
+ return;
221
+ throw new Error(`${where} declares "operations", and a kayan's operations are its own: write them under targets: { service: { operations: { ...defaultOperations } } } on each kayan.`);
222
+ };
223
+ noOperations('The language', language);
224
+ const stray = serviceKeys.find((key) => Object.hasOwn(language, key));
225
+ if (stray) {
226
+ throw new Error(`"${stray}" is not a key of the language: it is a key of the service target. Move it into targets.service: serviceTarget({ ... }).`);
227
+ }
228
+ const serviceTargets = Object.entries(language.targets).filter(([, target]) => isService(target));
229
+ if (serviceTargets.length === 0) {
230
+ throw new Error('This build declares no target of capability "service", and a build with no runner publishes nothing. Add service: serviceTarget({ authorize, depth, defaultLimit, maxLimit, joinLimit, bulkLimit, store, schema }) to targets.');
231
+ }
232
+ if (serviceTargets.length > 1) {
233
+ throw new Error(`Targets "${serviceTargets.map(([name]) => name).join('" and "')}" both declare capability "service", and a build has exactly one service target. Drop one of them.`);
234
+ }
235
+ if (serviceTargets[0][0] !== 'service') {
236
+ throw new Error(`Target "${serviceTargets[0][0]}" declares capability "service" and has to be named "service" in targets, because one word names the runner everywhere.`);
237
+ }
238
+ const service = serviceTargetOf(language);
239
+ // For a runner written as data, which no type reached. Core picks no cap, page size or
240
+ // bulk ceiling of its own.
241
+ noOperations('The service target', service);
242
+ const absent = ['authorize', 'store', 'schema', 'depth', ...pageKeys].find((key) => service[key] === undefined);
243
+ if (absent) {
244
+ throw new Error(`The service target declares no "${absent}", and every build's runner has one. Add it to serviceTarget({ ... }).`);
245
+ }
246
+ // Checked before `resolve` and the screens read `maxLimit`, so a bad one is refused in its own words.
247
+ for (const key of pageKeys) {
248
+ if (!Number.isInteger(service[key]) || service[key] < 1)
249
+ throw new Error(`service.${key} must be a positive integer.`);
250
+ }
251
+ // A page the runner opens on its own may not be larger than the most a caller may ask for.
252
+ for (const key of ['defaultLimit', 'joinLimit']) {
253
+ if (service[key] > service.maxLimit)
254
+ throw new Error(`service.${key} is ${service[key]}, past service.maxLimit ${service.maxLimit}.`);
255
+ }
256
+ const transport = ['http', 'orpc'].find((key) => Object.hasOwn(service, key));
257
+ if (transport) {
258
+ throw new Error(`The service target declares "${transport}", and transport is not the runner's: it is the list of plugs buildAsas takes under "http".`);
259
+ }
260
+ const resolved = resolve(declared, language);
261
+ // `tenant` is asked at a request, once the scope below is built.
262
+ const stored = resolveStorage({ language, resolved, storeOf: (one) => buildTargets[one.store], active: (actor) => tenant.active(actor) });
263
+ const storage = stored[0]?.storage.config;
264
+ // Set on every field before any schema is derived, for a value that points at a file from inside it.
265
+ const filesIn = stored.map((one) => one.name);
266
+ for (const field of resolved.flatMap((one) => Object.values(one.fields)))
267
+ field.filesIn = filesIn;
268
+ // Checked once, so a bad screen is refused here rather than at the first page drawn.
269
+ const screens = screensOf(language, resolved);
270
+ const hasAudit = (list) => list?.some((step) => sealOf(step) === 'audit') ?? false;
271
+ if (!service.audit) {
272
+ if ([service.beforeSteps, service.afterSteps, service.afterCommit].some(hasAudit)) {
273
+ throw new Error('The service target lists the "audit" step without an audit sink. Declare audit on the service target or drop the step.');
274
+ }
275
+ for (const kayan of resolved) {
276
+ for (const [name, operation] of Object.entries(kayan.operations)) {
277
+ if (hasAudit(operation.afterSteps)) {
278
+ throw new Error(`Kayan "${kayan.name}" operation "${name}" lists the "audit" step without an audit sink. Declare audit on the service target or drop the step.`);
279
+ }
280
+ }
281
+ }
282
+ }
283
+ // Derived once and never stored, so nothing drifts from the declarations. In Better Auth's
284
+ // `Statements` shape, so `createAccessControl` takes it as written.
285
+ const statements = Object.fromEntries(resolved.map((one) => [
286
+ one.name,
287
+ [...Object.keys(one.operations), ...Object.keys(one.fields).map((field) => `fields.${field}`)],
288
+ ]));
289
+ // One key per screen, which is all that decides who enters it. `resolve` reserves the kayan name `screen`.
290
+ if (Object.keys(screens).length)
291
+ statements.screen = Object.keys(screens);
292
+ // Unioned, because a resource may be both a kayan and a provider's: dropping either
293
+ // half would refuse a question one of them asks.
294
+ for (const [resource, actions] of Object.entries(declaredStatements ?? {})) {
295
+ statements[resource] = [...new Set([...(statements[resource] ?? []), ...actions])];
296
+ }
297
+ const catalog = Object.entries(statements).flatMap(([name, actions]) => actions.map((action) => `${name}.${action}`));
298
+ // Frozen down to each action list: one array is `asas.catalog` and every `tools.catalog`,
299
+ // so a push would widen what `grantedPermissions` measures against, project-wide.
300
+ for (const actions of Object.values(statements))
301
+ Object.freeze(actions);
302
+ Object.freeze(statements);
303
+ Object.freeze(catalog);
304
+ // A provider's own action is left out: a role may hold it for the provider alone.
305
+ const operationKeys = Object.freeze(Object.fromEntries(resolved.flatMap((one) => Object.keys(one.operations)
306
+ .filter((action) => !(declaredStatements?.[one.name] ?? []).includes(action))
307
+ .map((action) => [`${one.name}.${action}`, one.name]))));
308
+ // Kept against the catalog, so `grantedPermissions` reads it without a member of its own.
309
+ keepOperationKeys(catalog, operationKeys);
310
+ // A key nothing can ask for is a typo that would otherwise show only as a denial in production.
311
+ const known = new Set(catalog);
312
+ for (const table of new Set(resolved.map((one) => rowsOf(one.access)?.rows).filter((one) => !!one))) {
313
+ for (const [role, row] of Object.entries(table)) {
314
+ if (row.permissions === undefined || row.permissions === '*')
315
+ continue;
316
+ for (const key of row.permissions) {
317
+ if (known.has(key))
318
+ continue;
319
+ throw new Error(`Access row "${role}" holds permission "${key}", which is not a key of this project's catalog. A key is "<kayan>.<operation>", "<kayan>.fields.<field>" or "screen.<screen>", and this project's are: ${catalog.join(', ')}.`);
320
+ }
321
+ }
322
+ }
323
+ // Any other key the report never answers, so the entry would hide from everyone but `'*'`.
324
+ for (const [name, screen] of Object.entries(screens)) {
325
+ for (const [page, one] of Object.entries(screen.pages)) {
326
+ if (one.permission === undefined)
327
+ continue;
328
+ const dot = one.permission.indexOf('.');
329
+ const [resource, action] = [one.permission.slice(0, dot), one.permission.slice(dot + 1)];
330
+ const answered = resource === 'screen' ? Object.hasOwn(screens, action) : Object.hasOwn(resolved.find((kayan) => kayan.name === resource)?.operations ?? {}, action);
331
+ if (!answered) {
332
+ throw new Error(`Screen "${name}" page "${page}" permission "${one.permission}" is neither screen.<screen> of this build nor <kayan>.<operation> of a kayan it holds, and the nav could never show the page to anyone but '*'.`);
333
+ }
334
+ }
335
+ }
336
+ // Exactly one: a second would be a second answer to one question with no rule to pick
337
+ // between them. A build with none keeps the access callbacks alone.
338
+ const accessTargets = Object.entries(language.targets).filter(([, target]) => isAccess(target));
339
+ if (accessTargets.length > 1) {
340
+ throw new Error(`Targets "${accessTargets.map(([name]) => name).join('" and "')}" both declare capability "access", and a build has exactly one access target. Drop one of them.`);
341
+ }
342
+ const accessTarget = accessTargetOf(language);
343
+ // A typo in a system role's keys, or a field renamed under it, grants nothing and would
344
+ // otherwise say nothing.
345
+ for (const [role, { permissions }] of Object.entries(accessTarget?.roles ?? {})) {
346
+ if (permissions === '*')
347
+ continue;
348
+ for (const key of permissions) {
349
+ if (known.has(key))
350
+ continue;
351
+ throw new Error(`System role "${role}" holds permission "${key}", which is not a key of this project's catalog. A key is "<kayan>.<operation>", "<kayan>.fields.<field>" or "screen.<screen>", and this project's are: ${catalog.join(', ')}.`);
352
+ }
353
+ const fieldless = fieldlessOperation(permissions, operationKeys);
354
+ if (fieldless) {
355
+ throw new Error(`System role "${role}" holds operation key "${fieldless.key}" of kayan "${fieldless.kayan}" and no "${fieldless.kayan}.fields.<field>" key, so every read of it is refused for its fields. Grant the fields it reads, or drop the operation.`);
356
+ }
357
+ }
358
+ const registered = new Map(declared.map((one) => [one.name, one]));
359
+ const resolvedByName = new Map(resolved.map((one) => [one.name, one]));
360
+ // Wired here, not inside one provider, so no provider is a second source of keys for
361
+ // another. A target that throws takes the whole answer: half an answer is an unresolved grant.
362
+ const buildTargets = { ...language.targets };
363
+ const accessStore = () => buildTargets[service.store];
364
+ // The target reads role rows by table name with no soft-delete filter, so a kayan it reads
365
+ // must exist here and carry no mark, or a deleted role would keep granting.
366
+ for (const name of accessTarget?.reads ?? []) {
367
+ const roleKayan = resolvedByName.get(name);
368
+ if (!roleKayan) {
369
+ throw new Error(`The access target reads kayan "${name}", which this build does not declare. Add it to the buildAsas kayan list, point the target at a kayan this build has, or drop the target.`);
370
+ }
371
+ if (!roleKayan.softDelete)
372
+ continue;
373
+ throw new Error(`The access target reads kayan "${roleKayan.name}", which carries the service target's soft-delete field "${roleKayan.softDelete}". A role row the delete marked would still grant its keys, because the target is handed a store and a table name and never a kayan. Drop the field from that kayan, or drop the target.`);
374
+ }
375
+ const assigned = accessTarget
376
+ ? (args) => accessTarget.resolve({ store: accessStore(), ...args, ownership: (name) => resolvedByName.get(name)?.tenant })
377
+ : undefined;
378
+ // A target that holds rows is a store, and each one is handed only the kayan
379
+ // that name it. A target that just shapes a field has nothing to initialise.
380
+ const stores = Object.entries(language.targets).filter(([, target]) => isStore(target));
381
+ // Every store's `init` runs in registration order, which holds while no `init` consumes another's output.
382
+ for (const [at, [name, store]] of stores.entries()) {
383
+ const own = store.init(resolved.filter((one) => one.store === name)) ?? store;
384
+ stores[at] = [name, own];
385
+ buildTargets[name] = own;
386
+ }
387
+ const cap = service.depth;
388
+ if (!Number.isInteger(cap) || cap < 1)
389
+ throw new Error('service.depth must be a positive integer.');
390
+ const registeredKayanName = (label, value) => {
391
+ if (!value)
392
+ return undefined;
393
+ const original = registered.get(value.name);
394
+ if (!original) {
395
+ throw new Error(`${label} "${value.name}" is not in the buildAsas kayan list.`);
396
+ }
397
+ if (original !== value) {
398
+ throw new Error(`${label} "${value.name}" is not the registered kayan object.`);
399
+ }
400
+ return value.name;
401
+ };
402
+ const adminKayan = registeredKayanName('auth.admin.kayan', auth?.admin?.kayan);
403
+ const admin = auth ? { access: auth.admin?.access, kayan: adminKayan } : undefined;
404
+ const adminAuth = adminKayan ? registered.get(adminKayan)?.auth : undefined;
405
+ // Nothing is manufactured: an admin kayan no provider authenticates can never produce an actor.
406
+ if (adminKayan && !adminAuth) {
407
+ throw new Error(`auth.admin names kayan "${adminKayan}", which carries no auth.`);
408
+ }
409
+ // Only the shipped declarations mark their actor; marking a hand-written strategy's actor
410
+ // would vouch for its `strategy` and `role`, and so for the layer above every tenant.
411
+ if (adminAuth && !isAuthDeclaration(adminAuth)) {
412
+ throw new Error(`auth.admin names kayan "${adminKayan}", whose auth is a strategy this project wrote. A hand-written strategy cannot authenticate the admin kayan, because markAuthActor is not exported and it cannot mark the actor it returns as this kayan's. Give that kayan betterAuth({ server }) or local({ ... }).`);
413
+ }
414
+ // The service target's `authorize` is held to `assertWhere` like any access callback;
415
+ // a malformed result is the project's own 500, never the store reading it as a column.
416
+ // A second instance's sessions would name organizations of a table the tenant field never points at.
417
+ const betterAuthKayan = declared.filter((one) => one.auth && isAuthDeclaration(one.auth) && one.auth.provider === 'better-auth').map((one) => one.name);
418
+ const owned = resolved.find((one) => one.tenant);
419
+ if (owned && betterAuthKayan.length > 1) {
420
+ throw new Error(`Kayan "${betterAuthKayan.join('" and "')}" each carry Better Auth, and kayan "${owned.name}" belongs to the organization a session names. A build with a tenant-owned kayan carries one Better Auth kayan.`);
421
+ }
422
+ const tenant = tenancy({ language, assigned, onError: (error) => auth?.onError?.(error), collection: betterAuthKayan[0] });
423
+ // The tenant scope goes under both authorizers, so replacing either cannot leave it out.
424
+ const authorize = tenant.scoped(service.authorize === defaultAuthorize
425
+ ? defaultAuthorize
426
+ : async (args) => {
427
+ const own = await service.authorize(args);
428
+ return combineWhere(await defaultAuthorize(args), own === undefined ? undefined : assertWhere(args.kayan, own));
429
+ });
430
+ let built;
431
+ const assertNoOptions = (value) => {
432
+ if (value === undefined)
433
+ return;
434
+ // Two faults, two sentences.
435
+ if (value && typeof value === 'object' && ('actor' in value || 'context' in value)) {
436
+ throw new Error('Bound kayan tools inherit the current user and context; do not pass actor or context options.');
437
+ }
438
+ throw new Error('Bound kayan tools do not accept operation options.');
439
+ };
440
+ const boundCache = new Map();
441
+ const boundKayans = (mode) => {
442
+ const cached = boundCache.get(mode);
443
+ if (cached)
444
+ return cached;
445
+ // A global is not in the bound registry.
446
+ const registry = Object.freeze(Object.fromEntries(resolved.filter((one) => !one.single).map((one) => {
447
+ const publicKayan = built[one.name].operations;
448
+ const methods = Object.create(null);
449
+ for (const [operation, declaration] of Object.entries(one.operations)) {
450
+ if (mode === 'read' && declaration.effect !== 'read')
451
+ continue;
452
+ const method = publicKayan[operation];
453
+ methods[operation] = (input, options) => {
454
+ assertNoOptions(options);
455
+ return method(input);
456
+ };
457
+ }
458
+ return [one.name, Object.freeze(methods)];
459
+ })));
460
+ boundCache.set(mode, registry);
461
+ return registry;
462
+ };
463
+ // The object handed in must be the one this build registered, so a declaration from
464
+ // another build is refused by name.
465
+ const boundOf = (mode) => ((kayan) => {
466
+ const name = registeredKayanName('tools.of', kayan);
467
+ if (!name)
468
+ throw new Error('tools.of takes a kayan this build registered, and was handed none.');
469
+ if (registered.get(name).single)
470
+ throw new Error(`tools.of "${name}" is a global, which a step does not reach through its tools.`);
471
+ return boundKayans(mode)[name];
472
+ });
473
+ /** Every kayan carrying a provider, in the order the build lists them. */
474
+ const authenticating = declared.filter((one) => one.auth !== undefined);
475
+ for (const one of resolved) {
476
+ const local = !!one.auth && isAuthDeclaration(one.auth) && one.auth.provider === 'local';
477
+ if (!local && holdsVerificationSteps(one)) {
478
+ throw new Error(`kayan "${one.name}" holds the verification steps of localAuthOperations and carries no auth: local(...). Carry local(...), or leave localAuthOperations out of its operations.`);
479
+ }
480
+ }
481
+ const operationTable = [];
482
+ built = Object.fromEntries(resolved.map((one) => {
483
+ // The shapes are the schema target's own assembly; core validates them through `~standard`.
484
+ const target = language.targets[one.schema];
485
+ const schemas = target.derive(one.fields);
486
+ one.rowShape = schemas.row;
487
+ // And a versions read's own, over the columns of the versions table.
488
+ if (one.versions)
489
+ one.versions.kayan.rowShape = target.derive(one.versions.kayan.fields).row;
490
+ // Named here rather than at the first such read.
491
+ if (!schemas.byID) {
492
+ throw new Error(`Kayan "${one.name}" names schema target "${one.schema}", which derives no byID shape, which is what a read of one row parses. Derive "byID" beside "id": the id and an optional depth.`);
493
+ }
494
+ // A draft is parsed and read back through a shape the target has to
495
+ // derive, which is the kinds without the rules.
496
+ if (one.versions?.drafts && !schemas.draft) {
497
+ throw new Error(`Kayan "${one.name}" keeps drafts, and its schema target "${one.schema}" derives no draft shape, which is what a draft is parsed and read back through. Derive "draft" beside "row", or keep versions without drafts.`);
498
+ }
499
+ // The id is left out because the runner writes it itself.
500
+ const { id: _id, ...withoutId } = one.fields;
501
+ const singleUpdate = one.single ? target.derive(withoutId).update : undefined;
502
+ const store = buildTargets[one.store];
503
+ const read = readers({
504
+ kayan: one,
505
+ store,
506
+ cap,
507
+ defaultLimit: service.defaultLimit,
508
+ joinLimit: service.joinLimit,
509
+ admin,
510
+ authorize,
511
+ openTenant: tenant.open,
512
+ kayans: () => boundKayans('read'),
513
+ of: boundOf('read'),
514
+ audit: service.audit,
515
+ catalog,
516
+ });
517
+ const { methods, table } = operations(one, store, schemas, singleUpdate, cap, read, (effect) => boundKayans(effect === 'read' ? 'read' : 'write'), (effect) => boundOf(effect === 'read' ? 'read' : 'write'), service.audit, admin, authorize, catalog, service.context !== undefined, service, { active: tenant.active, seal: (writes) => tenant.sealWrites({ writes, find: store.find.bind(store), kayanOf: (name) => resolvedByName.get(name) }) });
518
+ operationTable.push(...table);
519
+ const bound = Object.freeze(methods);
520
+ // Only what this kayan's own targets built for it; their configuration stays on `asas.targets`.
521
+ const targets = {
522
+ service: Object.freeze({ operations: bound }),
523
+ [one.schema]: Object.freeze({ schemas }),
524
+ ...(store.tables && Object.hasOwn(store.tables, one.name)
525
+ ? { [one.store]: Object.freeze({ table: store.tables[one.name] }) }
526
+ : {}),
527
+ };
528
+ // Written out, not spread, so no access callback, thunk or target configuration appears
529
+ // under a runtime output's name. `fields` is frozen below, so its reach reads and never writes.
530
+ return [one.name, Object.freeze({ name: one.name, fields: one.fields, operations: bound, targets: Object.freeze(targets) })];
531
+ }));
532
+ // Built once, outside any provider: the route answers for whatever actor the loop resolved.
533
+ const reportable = {
534
+ kayan: resolved,
535
+ // Asked once per operation name, since `authorize` stands the permission layer aside with
536
+ // no name, which would report an effect the runner refuses. Each name's answer is returned too.
537
+ can: async ({ kayan: one, effect, actor: actorForAccess }) => {
538
+ const named = operationTable
539
+ .filter((operation) => operation.kayan === one.name && operation.effect === effect)
540
+ .map((operation) => operation.name);
541
+ const operations = {};
542
+ // Platform staff in the platform app read a tenant-owned kayan across every organization,
543
+ // so its read is reported as they make it; anyone else is answered in the active one.
544
+ const reach = effect === 'read' && one.tenant && !one.single
545
+ ? await tenant.open({ actor: actorForAccess, reach: 'all', kayan: one }).catch(() => undefined)
546
+ : undefined;
547
+ const ask = (operation) => authorize({ admin: admin, actor: actorForAccess, data: {}, effect, kayan: one, operation });
548
+ for (const operation of named.length ? named : [undefined]) {
549
+ try {
550
+ await (reach ? running.run({ context: Object.freeze({ source: 'local', depth: 0, chain: [] }), user: actorForAccess, tenant: reach }, () => ask(operation)) : ask(operation));
551
+ if (operation)
552
+ operations[operation] = true;
553
+ else
554
+ return { allowed: true, operations };
555
+ }
556
+ catch {
557
+ // Each name is asked whatever an earlier one answered, because the report names them all.
558
+ if (operation)
559
+ operations[operation] = false;
560
+ }
561
+ }
562
+ return { allowed: Object.values(operations).some(Boolean), operations };
563
+ },
564
+ canAccessAdmin: async (actorForAccess) => markAdminAnswer(actorForAccess, !!(await defaultAccess(admin, actorForAccess))),
565
+ // Read off the trusted snapshot, so an actor with none enters nothing.
566
+ screens: async (actorForAccess) => {
567
+ const names = Object.keys(screens);
568
+ if (!accessTarget)
569
+ return (await defaultAccess(admin, actorForAccess)) ? names : [];
570
+ const held = actorPermissions(actorForAccess);
571
+ return held === true ? names : names.filter((name) => held?.has(`screen.${name}`) === true);
572
+ },
573
+ };
574
+ // Precedence is the order the build lists its kayan: the loop takes the first non-null actor.
575
+ if (authenticating.length && typeof auth?.onError !== 'function') {
576
+ throw new Error(`buildAsas was handed no "auth.onError", and kayan "${authenticating[0].name}" carries auth. Core reports no failed strategy of its own: name the project's own, such as auth: { onError: console.error }.`);
577
+ }
578
+ const onError = (error) => auth.onError(error);
579
+ const strategies = (() => {
580
+ const list = [];
581
+ // Without this freeze a `roles` function reassigning `v.catalog` would be seen by a
582
+ // `server` factory, and the target and the provider would disagree.
583
+ const vocabulary = Object.freeze({ catalog: catalog, statements });
584
+ for (const one of authenticating) {
585
+ const declaration = one.auth;
586
+ if (!isAuthDeclaration(declaration)) {
587
+ list.push(declaration);
588
+ continue;
589
+ }
590
+ if (declaration.provider === 'better-auth') {
591
+ const { server } = declaration;
592
+ if (typeof server !== 'function')
593
+ throw new Error(`kayan "${one.name}" carries auth: betterAuth({ server }), and server must be a factory.`);
594
+ list.push(betterAuthStrategy({ server: server(buildTargets, vocabulary), collection: one.name, assigned, onError }));
595
+ continue;
596
+ }
597
+ if (declaration.provider !== 'local') {
598
+ throw new Error(`kayan "${one.name}" carries an auth that is neither a strategy, which is an object with an \`authenticate\`, nor one of the shipped declarations, betterAuth({ server }) and local({ ... }).`);
599
+ }
600
+ const options = declaration.options;
601
+ // First registration is served, so it needs the project's context function too.
602
+ if (service.context !== undefined && typeof options.context !== 'function') {
603
+ throw new Error(`kayan "${one.name}" carries auth: local(...) with no context function, and this build's runner declares a context, which first registration would hand its steps without. Give it local({ context }).`);
604
+ }
605
+ const local = resolvedByName.get(one.name);
606
+ const store = buildTargets[local.store];
607
+ if (!isStore(store) || !isLocalAuthTarget(store)) {
608
+ throw new Error(`kayan "${local.name}" carries auth: local(...) and must use a store declaring sql: "drizzle" with db and tables.`);
609
+ }
610
+ list.push(localAuthStrategy({
611
+ options,
612
+ kayan: local,
613
+ target: store,
614
+ writes: built,
615
+ projectUser: (row, actor) => projectReadableFields({ kayan: local, row, actor }),
616
+ permissions: reportable,
617
+ assigned,
618
+ onError,
619
+ }));
620
+ }
621
+ // Two strategies sharing a name would share a prefix, and the second's endpoints would
622
+ // be unreachable with nothing said.
623
+ const seen = new Set();
624
+ for (const strategy of list) {
625
+ if (seen.has(strategy.name)) {
626
+ throw new Error(`Two strategies of this build are named "${strategy.name}", and each would answer /api/auth/${strategy.name}/*. Rename one of them.`);
627
+ }
628
+ seen.add(strategy.name);
629
+ }
630
+ return list;
631
+ })();
632
+ // The build's admin answer is recorded on the actor, so a field callback can read it.
633
+ const resolveActor = async (headers) => {
634
+ const found = await actor({ strategies: strategies, headers, onError });
635
+ if (found)
636
+ markAdminAnswer(found, !!(await defaultAccess(admin, found)));
637
+ return found;
638
+ };
639
+ // Cast over the table's static view, which a plug reads from the type only.
640
+ const plugs = http.map((make) => make(operationTable));
641
+ // A name that answers for two plugs names neither.
642
+ const plugNames = new Set();
643
+ for (const one of plugs) {
644
+ if (plugNames.has(one.name)) {
645
+ throw new Error(`Two http plugs are named "${one.name}". A plug's name is what a kayan names to opt out of it, so it names one plug of the list.`);
646
+ }
647
+ plugNames.add(one.name);
648
+ }
649
+ /** Assembled on the first read of `messages`, and kept. */
650
+ let registry;
651
+ // Read through the kayan's own `findByID`, so its access, tenant and soft-delete mark decide.
652
+ const storageServe = storage && stored.length
653
+ ? storageRoutes({
654
+ kayan: stored,
655
+ config: storage,
656
+ read: async ({ kayan: name, id, actor }) => {
657
+ const findByID = built[name].operations.findByID;
658
+ return findByID ? (await findByID({ id }, { actor })) : null;
659
+ },
660
+ authorize,
661
+ admin: admin,
662
+ })
663
+ : undefined;
664
+ const asas = {
665
+ /** Every permission key this project has: one `<kayan>.<operation>` per operation, one `<kayan>.fields.<field>` per field, one `screen.<screen>` per screen. */
666
+ catalog: catalog,
667
+ /** Every scope of copy this project has. A getter: only the CLI reads it, and it calls the project's own resolver. */
668
+ get messages() {
669
+ return (registry ??= messagesOf({ language, kayanList: resolved, resolver: messages }));
670
+ },
671
+ /** The same catalog in Better Auth's shape, for `createAccessControl(asas.statements)`. */
672
+ statements: statements,
673
+ /** Every built operation, flat. Its `procedures` view is a type, keyed by kayan then operation. */
674
+ operations: operationTable,
675
+ /** The router the first plug built, which a typed client is built from; `undefined` for `fetchHttp()`. */
676
+ router: plugs[0]?.router,
677
+ /** Every target as the language passed it, so `asas.targets.drizzle.db` is drizzle. A screen also carries its projection under `client`. */
678
+ targets: Object.fromEntries(Object.entries(buildTargets).map(([name, target]) => [name, isScreen(target) ? { ...target, client: screens[name] } : target])),
679
+ // Dynamic keys were checked during resolution; this is their one typed
680
+ // construction boundary, after every operation gained a runtime row proof.
681
+ kayan: Object.fromEntries(Object.entries(built).filter(([name]) => !globalNames.has(name))),
682
+ /** One row per global, under `findOne` and `update`. */
683
+ global: Object.fromEntries(Object.entries(built).filter(([name]) => globalNames.has(name))),
684
+ /** The strategies in the order the loop tries them, and the loop itself. */
685
+ auth: {
686
+ strategies: strategies,
687
+ actor: resolveActor,
688
+ },
689
+ handler: async (request) => {
690
+ const pathname = new URL(request.url).pathname;
691
+ for (const strategy of strategies) {
692
+ if (!strategy.handler)
693
+ continue;
694
+ const prefix = `/api/auth/${strategy.name}`;
695
+ if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
696
+ // Read here first, so a client that goes away mid-body is answered 499 here rather than
697
+ // logged by the provider as its own 500.
698
+ if (!request.body)
699
+ return strategy.handler(request);
700
+ let body;
701
+ try {
702
+ body = await request.arrayBuffer();
703
+ }
704
+ catch (thrown) {
705
+ if (request.signal.aborted)
706
+ return new Response(null, { status: 499 });
707
+ throw thrown;
708
+ }
709
+ return strategy.handler(new Request(request.url, { method: request.method, headers: request.headers, body, signal: request.signal }));
710
+ }
711
+ }
712
+ // The report is the handler's own route, so a build that drops a plug keeps it. One actor
713
+ // thunk for the whole request: each strategy-loop run is a store read.
714
+ let resolved;
715
+ const who = () => (resolved ??= resolveActor(request.headers));
716
+ if (pathname === '/api/access' && request.method === 'GET') {
717
+ return Response.json(await accessReport(reportable, await who()), {
718
+ headers: { 'content-type': 'application/json' },
719
+ });
720
+ }
721
+ // The handler's own for the reason the report is.
722
+ const filed = await storageServe?.(request, who);
723
+ if (filed)
724
+ return filed;
725
+ // The auth arm first, since `resolve` reserves `auth` under the same `/api`. A plug that
726
+ // matched nothing answers `null` and the next is asked.
727
+ for (const plug of plugs) {
728
+ const answered = await plug.handle(request, who);
729
+ if (answered)
730
+ return answered;
731
+ }
732
+ return new Response('Not found', { status: 404 });
733
+ },
734
+ connect: async ({ readOnly } = {}) => {
735
+ // `readOnly` writes nothing: no push.
736
+ for (const [, store] of stores)
737
+ await store.connect({ readOnly });
738
+ },
739
+ destroy: async () => {
740
+ for (const [, store] of stores)
741
+ await store.destroy();
742
+ },
743
+ };
744
+ // Frozen deep once nothing of the build writes to it: the runner reads every record at call
745
+ // time. Library objects are left alone, since freezing them breaks them.
746
+ const shapers = Object.entries(language.targets).filter(([, target]) => !isAccess(target) && !isScreen(target) && !isService(target) && !isVersions(target) && !isStorage(target) && !isTenant(target));
747
+ const library = new Set(operationTable.map((one) => one.input));
748
+ const foreign = (one) => {
749
+ library.add(one.custom);
750
+ for (const [name] of shapers)
751
+ library.add(one.targets?.[name]);
752
+ };
753
+ for (const one of [...declared, ...resolved, ...Object.values(built)]) {
754
+ foreign(one);
755
+ library.add(one.auth);
756
+ for (const field of Object.values(one.fields))
757
+ foreign(field);
758
+ for (const operation of [...Object.values(one.operations ?? {}), ...Object.values(one.targets?.service?.operations ?? {})]) {
759
+ foreign(operation);
760
+ library.add(operation.output);
761
+ for (const block of Object.values(operation.targets ?? {}))
762
+ for (const field of Object.values(block?.fields ?? {}))
763
+ foreign(field);
764
+ }
765
+ }
766
+ const seen = new WeakSet();
767
+ const freeze = (value) => {
768
+ if (!value || typeof value !== 'object' || seen.has(value) || library.has(value))
769
+ return;
770
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
771
+ return;
772
+ seen.add(value);
773
+ Object.freeze(value);
774
+ for (const nested of Object.values(value))
775
+ freeze(nested);
776
+ };
777
+ for (const record of [declared, resolved, built, asas.kayan, asas.global, operationTable, screens])
778
+ freeze(record);
779
+ // Keyed by the build, off its public shape.
780
+ resolvedKayans.set(asas, Object.freeze(Object.fromEntries(resolved.map((one) => [one.name, one]))));
781
+ if (storage && stored.length)
782
+ recordStorage(asas, { config: storage, kayan: stored, storeOf: (one) => buildTargets[one.store] });
783
+ // A strategy pushed onto the list, or a handler or target swapped in, would answer every later request.
784
+ for (const record of [strategies, asas.auth, asas.targets, asas])
785
+ Object.freeze(record);
786
+ return asas;
787
+ };