velocious 1.0.473 → 1.0.475

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 (96) hide show
  1. package/README.md +7 -2
  2. package/build/authorization/base-resource.js +99 -0
  3. package/build/configuration-types.js +41 -3
  4. package/build/configuration.js +41 -1
  5. package/build/database/drivers/base.js +51 -3
  6. package/build/database/drivers/mysql/index.js +74 -7
  7. package/build/database/drivers/sqlite/connection-sql-js.js +16 -5
  8. package/build/database/drivers/sqlite/index.web.js +29 -15
  9. package/build/database/drivers/sqlite/web-persistence.js +521 -0
  10. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
  11. package/build/frontend-model-controller.js +139 -16
  12. package/build/frontend-model-resource/base-resource.js +258 -46
  13. package/build/frontend-model-resource/velocious-attachment-resource.js +0 -3
  14. package/build/frontend-models/base.js +146 -2
  15. package/build/frontend-models/query.js +3 -3
  16. package/build/frontend-models/resource-definition.js +400 -23
  17. package/build/routes/hooks/frontend-model-command-route-hook.js +8 -0
  18. package/build/src/authorization/base-resource.d.ts +41 -0
  19. package/build/src/authorization/base-resource.d.ts.map +1 -1
  20. package/build/src/authorization/base-resource.js +88 -1
  21. package/build/src/configuration-types.d.ts +128 -5
  22. package/build/src/configuration-types.d.ts.map +1 -1
  23. package/build/src/configuration-types.js +38 -4
  24. package/build/src/configuration.d.ts +18 -1
  25. package/build/src/configuration.d.ts.map +1 -1
  26. package/build/src/configuration.js +37 -2
  27. package/build/src/database/drivers/base.d.ts +30 -0
  28. package/build/src/database/drivers/base.d.ts.map +1 -1
  29. package/build/src/database/drivers/base.js +47 -4
  30. package/build/src/database/drivers/mysql/index.d.ts +32 -3
  31. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  32. package/build/src/database/drivers/mysql/index.js +65 -7
  33. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +8 -1
  34. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  35. package/build/src/database/drivers/sqlite/connection-sql-js.js +15 -6
  36. package/build/src/database/drivers/sqlite/index.web.d.ts +5 -5
  37. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  38. package/build/src/database/drivers/sqlite/index.web.js +27 -13
  39. package/build/src/database/drivers/sqlite/web-persistence.d.ts +77 -0
  40. package/build/src/database/drivers/sqlite/web-persistence.d.ts.map +1 -0
  41. package/build/src/database/drivers/sqlite/web-persistence.js +464 -0
  42. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +30 -0
  43. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  44. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +54 -2
  45. package/build/src/frontend-model-controller.d.ts +34 -0
  46. package/build/src/frontend-model-controller.d.ts.map +1 -1
  47. package/build/src/frontend-model-controller.js +125 -17
  48. package/build/src/frontend-model-resource/base-resource.d.ts +69 -0
  49. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  50. package/build/src/frontend-model-resource/base-resource.js +247 -54
  51. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +0 -2
  52. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
  53. package/build/src/frontend-model-resource/velocious-attachment-resource.js +1 -3
  54. package/build/src/frontend-models/base.d.ts +29 -0
  55. package/build/src/frontend-models/base.d.ts.map +1 -1
  56. package/build/src/frontend-models/base.js +131 -3
  57. package/build/src/frontend-models/query.js +4 -4
  58. package/build/src/frontend-models/resource-definition.d.ts +6 -0
  59. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  60. package/build/src/frontend-models/resource-definition.js +355 -28
  61. package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
  62. package/build/src/routes/hooks/frontend-model-command-route-hook.js +8 -1
  63. package/build/src/sync/device-identity.d.ts +240 -0
  64. package/build/src/sync/device-identity.d.ts.map +1 -0
  65. package/build/src/sync/device-identity.js +454 -0
  66. package/build/src/sync/local-mutation-log.d.ts +212 -0
  67. package/build/src/sync/local-mutation-log.d.ts.map +1 -0
  68. package/build/src/sync/local-mutation-log.js +404 -0
  69. package/build/src/sync/offline-grant.d.ts +157 -0
  70. package/build/src/sync/offline-grant.d.ts.map +1 -0
  71. package/build/src/sync/offline-grant.js +292 -0
  72. package/build/sync/device-identity.js +503 -0
  73. package/build/sync/local-mutation-log.js +448 -0
  74. package/build/sync/offline-grant.js +327 -0
  75. package/build/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +3 -3
  77. package/scripts/tensorbuzz-retry +21 -0
  78. package/src/authorization/base-resource.js +99 -0
  79. package/src/configuration-types.js +41 -3
  80. package/src/configuration.js +41 -1
  81. package/src/database/drivers/base.js +51 -3
  82. package/src/database/drivers/mysql/index.js +74 -7
  83. package/src/database/drivers/sqlite/connection-sql-js.js +16 -5
  84. package/src/database/drivers/sqlite/index.web.js +29 -15
  85. package/src/database/drivers/sqlite/web-persistence.js +521 -0
  86. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
  87. package/src/frontend-model-controller.js +139 -16
  88. package/src/frontend-model-resource/base-resource.js +258 -46
  89. package/src/frontend-model-resource/velocious-attachment-resource.js +0 -3
  90. package/src/frontend-models/base.js +146 -2
  91. package/src/frontend-models/query.js +3 -3
  92. package/src/frontend-models/resource-definition.js +400 -23
  93. package/src/routes/hooks/frontend-model-command-route-hook.js +8 -0
  94. package/src/sync/device-identity.js +503 -0
  95. package/src/sync/local-mutation-log.js +448 -0
  96. package/src/sync/offline-grant.js +327 -0
@@ -129,14 +129,14 @@ export function normalizePreload(preload) {
129
129
  continue
130
130
  }
131
131
 
132
- throw new Error(`Invalid preload entry type: ${typeof entry}`)
132
+ throw frontendModelQueryError(`Invalid preload entry type: ${typeof entry}`)
133
133
  }
134
134
 
135
135
  return normalized
136
136
  }
137
137
 
138
138
  if (!isPlainObject(preload)) {
139
- throw new Error(`Invalid preload type: ${typeof preload}`)
139
+ throw frontendModelQueryError(`Invalid preload type: ${typeof preload}`)
140
140
  }
141
141
 
142
142
  /**
@@ -155,7 +155,7 @@ export function normalizePreload(preload) {
155
155
  continue
156
156
  }
157
157
 
158
- throw new Error(`Invalid preload value for ${relationshipName}: ${typeof relationshipPreload}`)
158
+ throw frontendModelQueryError(`Invalid preload value for ${relationshipName}: ${typeof relationshipPreload}`)
159
159
  }
160
160
 
161
161
  return normalized
@@ -5,6 +5,48 @@ import FrontendModelBaseResource from "../frontend-model-resource/base-resource.
5
5
  import restArgsError from "../utils/rest-args-error.js"
6
6
  import {validateFrontendModelResourceCommandName} from "./resource-config-validation.js"
7
7
 
8
+ const BASE_FRONTEND_MODEL_ABILITY_ACTIONS = ["create", "destroy", "read", "update"]
9
+ const SHA256_INITIAL_HASH = [
10
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
11
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
12
+ ]
13
+ const SHA256_K = [
14
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
15
+ 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
16
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
17
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
18
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
19
+ 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
20
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
21
+ 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
22
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
23
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
24
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
25
+ 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
26
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
27
+ 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
28
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
29
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
30
+ ]
31
+ const RESOURCE_STATIC_CONFIG_KEYS = new Set([
32
+ "abilities",
33
+ "attachments",
34
+ "attributes",
35
+ "builtInCollectionCommands",
36
+ "builtInMemberCommands",
37
+ "collectionCommands",
38
+ "commands",
39
+ "memberCommands",
40
+ "modelName",
41
+ "ModelClass",
42
+ "primaryKey",
43
+ "relationships",
44
+ "server",
45
+ "SharedResource",
46
+ "sync",
47
+ "translatedAttributes"
48
+ ])
49
+
8
50
  /**
9
51
  * Runs the frontendModelResourcesForBackendProject helper.
10
52
  * @param {import("../configuration-types.js").BackendProjectConfiguration} backendProject - Backend project config.
@@ -50,9 +92,74 @@ export function frontendModelResourceClassFromDefinition(resourceDefinition) {
50
92
  export function frontendModelResourceConfigurationFromDefinition(resourceDefinition) {
51
93
  if (!frontendModelResourceDefinitionIsClass(resourceDefinition)) return null
52
94
 
95
+ assertResourceConfigIsFrameworkDefined(resourceDefinition)
96
+
53
97
  return normalizeFrontendModelResourceConfiguration(resourceDefinition.resourceConfig())
54
98
  }
55
99
 
100
+ /**
101
+ * Ensures resources use declarative static config properties instead of overriding resourceConfig().
102
+ * @param {import("../configuration-types.js").FrontendModelResourceClassType} ResourceClass - Resource class.
103
+ * @param {Set<import("../configuration-types.js").FrontendModelResourceClassType>} [visited] - Already inspected shared resources.
104
+ * @returns {void}
105
+ */
106
+ function assertResourceConfigIsFrameworkDefined(ResourceClass, visited = new Set()) {
107
+ if (visited.has(ResourceClass)) return
108
+
109
+ visited.add(ResourceClass)
110
+ assertKnownResourceStaticConfigProperties(ResourceClass)
111
+
112
+ const owner = staticMethodOwnerFor(ResourceClass, "resourceConfig")
113
+
114
+ if (owner && owner !== FrontendModelBaseResource) {
115
+ throw new Error(`${ResourceClass.name} overrides static resourceConfig(), which is not supported. Use static resource properties instead.`)
116
+ }
117
+
118
+ const SharedResource = ResourceClass.sharedResourceClass()
119
+
120
+ if (SharedResource) assertResourceConfigIsFrameworkDefined(SharedResource, visited)
121
+ }
122
+
123
+ /**
124
+ * Ensures declarative static resource config does not silently ignore typos or removed keys.
125
+ * @param {import("../configuration-types.js").FrontendModelResourceClassType} ResourceClass - Resource class.
126
+ * @returns {void}
127
+ */
128
+ function assertKnownResourceStaticConfigProperties(ResourceClass) {
129
+ let currentClass = ResourceClass
130
+
131
+ while (currentClass && currentClass !== FrontendModelBaseResource && currentClass !== Function.prototype) {
132
+ /** @type {Record<string, ?>} */
133
+ const unknownStaticConfig = {}
134
+
135
+ for (const key of Object.keys(currentClass)) {
136
+ if (!RESOURCE_STATIC_CONFIG_KEYS.has(key)) unknownStaticConfig[key] = /** @type {Record<string, ?>} */ (/** @type {unknown} */ (currentClass))[key]
137
+ }
138
+
139
+ restArgsError(unknownStaticConfig)
140
+
141
+ currentClass = Object.getPrototypeOf(currentClass)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Locates which constructor owns a static method implementation.
147
+ * @param {import("../configuration-types.js").FrontendModelResourceClassType} ResourceClass - Resource class.
148
+ * @param {string} methodName - Method name.
149
+ * @returns {import("../configuration-types.js").FrontendModelResourceClassType | typeof FrontendModelBaseResource | null} - Class that owns the static method.
150
+ */
151
+ function staticMethodOwnerFor(ResourceClass, methodName) {
152
+ let currentClass = ResourceClass
153
+
154
+ while (currentClass && currentClass !== Function.prototype) {
155
+ if (Object.prototype.hasOwnProperty.call(currentClass, methodName)) return currentClass
156
+
157
+ currentClass = Object.getPrototypeOf(currentClass)
158
+ }
159
+
160
+ return null
161
+ }
162
+
56
163
  /**
57
164
  * Runs normalize frontend model resource configuration.
58
165
  * @param {import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Raw resource configuration.
@@ -73,7 +180,8 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
73
180
  "modelName",
74
181
  "primaryKey",
75
182
  "relationships",
76
- "server"
183
+ "server",
184
+ "sync"
77
185
  ]) {
78
186
  delete restArgs[key]
79
187
  }
@@ -81,6 +189,7 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
81
189
  restArgsError(restArgs)
82
190
 
83
191
  const normalizedCommands = normalizeFrontendModelResourceCommands(resourceConfiguration)
192
+ const sync = normalizeFrontendModelResourceSync(resourceConfiguration)
84
193
 
85
194
  return {
86
195
  ...resourceConfiguration,
@@ -92,7 +201,8 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
92
201
  // name, derived from `{name, args?, returnType?}` command entries. The
93
202
  // generator uses it to type each custom command method.
94
203
  commandMetadata: normalizedCommands.commandMetadata,
95
- memberCommands: normalizedCommands.memberCommands
204
+ memberCommands: normalizedCommands.memberCommands,
205
+ sync
96
206
  }
97
207
  }
98
208
 
@@ -102,36 +212,27 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
102
212
  * @returns {Record<string, string>} - Normalized abilities config.
103
213
  */
104
214
  function normalizeFrontendModelResourceAbilities(abilities) {
105
- if (abilities === undefined) {
106
- return defaultCrudAbilities()
107
- }
215
+ const normalized = defaultCrudAbilities()
216
+
217
+ if (abilities === undefined) return normalized
108
218
 
109
219
  if (!Array.isArray(abilities)) {
110
220
  throw new Error("Resource abilities must be an array of action names. Object form is no longer supported.")
111
221
  }
112
222
 
113
- if (abilities.includes("manage")) {
114
- return {
115
- create: "manage",
116
- destroy: "manage",
117
- find: "manage",
118
- index: "manage",
119
- update: "manage"
120
- }
223
+ const duplicatedBaseAbilities = abilities.filter((ability) => BASE_FRONTEND_MODEL_ABILITY_ACTIONS.includes(ability))
224
+
225
+ if (duplicatedBaseAbilities.length > 0) {
226
+ throw new Error(`Resource abilities must not include base actions: ${duplicatedBaseAbilities.join(", ")}`)
121
227
  }
122
228
 
123
- /**
124
- * Normalized.
125
- * @type {Record<string, string>} */
126
- const normalized = {}
229
+ for (const ability of abilities) {
230
+ if (typeof ability !== "string" || ability.length < 1) {
231
+ throw new Error("Resource abilities entries must be non-empty strings.")
232
+ }
127
233
 
128
- if (abilities.includes("create")) normalized.create = "create"
129
- if (abilities.includes("destroy")) normalized.destroy = "destroy"
130
- if (abilities.includes("read")) {
131
- normalized.find = "read"
132
- normalized.index = "read"
234
+ normalized[ability] = ability
133
235
  }
134
- if (abilities.includes("update")) normalized.update = "update"
135
236
 
136
237
  return normalized
137
238
  }
@@ -150,6 +251,282 @@ function defaultCrudAbilities() {
150
251
  }
151
252
  }
152
253
 
254
+ /**
255
+ * Builds a frontend-safe sync manifest for all sync-enabled frontend-model resources.
256
+ * @param {import("../configuration-types.js").BackendProjectConfiguration[]} backendProjects - Backend projects to scan.
257
+ * @returns {Record<string, import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration>} - Sync metadata keyed by model name.
258
+ */
259
+ export function frontendModelSyncManifestForBackendProjects(backendProjects) {
260
+ /** @type {Record<string, import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration>} */
261
+ const manifest = {}
262
+
263
+ for (const backendProject of backendProjects) {
264
+ const resources = frontendModelResourcesForBackendProject(backendProject)
265
+
266
+ for (const configuredModelName of Object.keys(resources).sort()) {
267
+ const resourceDefinition = resources[configuredModelName]
268
+ const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
269
+
270
+ if (!resourceConfiguration) continue
271
+ if (!resourceConfiguration.sync?.enabled) continue
272
+
273
+ const modelName = resourceConfiguration.modelName || configuredModelName
274
+
275
+ manifest[modelName] = resourceConfiguration.sync
276
+ }
277
+ }
278
+
279
+ return manifest
280
+ }
281
+
282
+ /**
283
+ * Normalizes sync policy metadata and computes a deterministic hash from safe policy inputs.
284
+ * @param {import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Raw resource configuration.
285
+ * @returns {import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration | undefined} - Frontend-safe sync metadata.
286
+ */
287
+ function normalizeFrontendModelResourceSync(resourceConfiguration) {
288
+ const sync = resourceConfiguration.sync
289
+
290
+ if (sync === undefined || sync === null) return undefined
291
+ if (sync === false) return {enabled: false, operations: [], policyHash: syncPolicyHash({enabled: false}), policyVersion: null}
292
+ if (sync === true) {
293
+ return normalizeFrontendModelResourceSync({
294
+ ...resourceConfiguration,
295
+ sync: {operations: ["index", "find"]}
296
+ })
297
+ }
298
+ if (!sync || typeof sync !== "object" || Array.isArray(sync)) {
299
+ throw new Error("Resource sync configuration must be true, false, or an object.")
300
+ }
301
+
302
+ const {enabled = true, metadata, operations, policy, policyVersion, ...rest} = /** @type {import("../configuration-types.js").FrontendModelResourceSyncConfiguration} */ (sync)
303
+
304
+ if (Object.keys(rest).length > 0) {
305
+ throw new Error(`Unexpected sync keys: ${Object.keys(rest).join(", ")}. Allowed: enabled, metadata, operations, policy, policyVersion`)
306
+ }
307
+ if (enabled !== true && enabled !== false) throw new Error("Resource sync enabled must be true or false when provided.")
308
+
309
+ const normalizedOperations = normalizeSyncOperations(operations)
310
+ const normalizedMetadata = metadata === undefined ? undefined : deterministicSyncJson({label: "metadata", value: metadata})
311
+ const normalizedPolicy = policy === undefined ? undefined : deterministicSyncJson({label: "policy", value: policy})
312
+ const normalizedPolicyVersion = policyVersion === undefined || policyVersion === null ? null : String(policyVersion)
313
+ const hashInput = {
314
+ enabled,
315
+ metadata: normalizedMetadata,
316
+ modelName: resourceConfiguration.modelName || null,
317
+ operations: normalizedOperations,
318
+ policy: normalizedPolicy,
319
+ policyVersion: normalizedPolicyVersion
320
+ }
321
+ /** @type {import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration} */
322
+ const normalized = {
323
+ enabled,
324
+ operations: normalizedOperations,
325
+ policyHash: syncPolicyHash(hashInput),
326
+ policyVersion: normalizedPolicyVersion
327
+ }
328
+
329
+ if (normalizedMetadata !== undefined) normalized.metadata = /** @type {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue>} */ (normalizedMetadata)
330
+
331
+ return normalized
332
+ }
333
+
334
+ /**
335
+ * Normalizes sync operations into a stable, duplicate-free list.
336
+ * @param {unknown} operations - Raw operations value.
337
+ * @returns {string[]} - Normalized operations.
338
+ */
339
+ function normalizeSyncOperations(operations) {
340
+ if (operations === undefined) return []
341
+ if (!Array.isArray(operations)) throw new Error("Resource sync operations must be an array of operation names.")
342
+
343
+ const normalized = operations.map((operation) => {
344
+ if (typeof operation !== "string" || operation.length < 1) throw new Error("Resource sync operations entries must be non-empty strings.")
345
+
346
+ return operation
347
+ })
348
+
349
+ return [...new Set(normalized)].sort()
350
+ }
351
+
352
+ /**
353
+ * Builds a deterministic policy hash.
354
+ * @param {unknown} value - Hash input.
355
+ * @returns {string} - sha256-prefixed hash.
356
+ */
357
+ function syncPolicyHash(value) {
358
+ return `sha256-${sha256Hex(stableJsonStringify(value))}`
359
+ }
360
+
361
+ /**
362
+ * Computes SHA-256 without importing Node-only crypto modules, keeping this
363
+ * resource-definition module safe for Expo/browser bundles.
364
+ * @param {string} message - UTF-8 message.
365
+ * @returns {string} - Hex digest.
366
+ */
367
+ function sha256Hex(message) {
368
+ const bytes = utf8Bytes(message)
369
+ const padded = [...bytes]
370
+ const bitLength = bytes.length * 8
371
+ const hash = [...SHA256_INITIAL_HASH]
372
+ /** @type {number[]} */
373
+ const words = new Array(64)
374
+
375
+ padded.push(0x80)
376
+ while (padded.length % 64 !== 56) padded.push(0)
377
+
378
+ const highLength = Math.floor(bitLength / 0x100000000)
379
+ const lowLength = bitLength >>> 0
380
+
381
+ for (const value of [highLength, lowLength]) {
382
+ padded.push((value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff)
383
+ }
384
+
385
+ for (let offset = 0; offset < padded.length; offset += 64) {
386
+ for (let i = 0; i < 16; i++) {
387
+ const index = offset + (i * 4)
388
+
389
+ words[i] = (((padded[index] || 0) << 24) | ((padded[index + 1] || 0) << 16) | ((padded[index + 2] || 0) << 8) | (padded[index + 3] || 0)) >>> 0
390
+ }
391
+
392
+ for (let i = 16; i < 64; i++) {
393
+ const s0 = rotateRight(words[i - 15], 7) ^ rotateRight(words[i - 15], 18) ^ (words[i - 15] >>> 3)
394
+ const s1 = rotateRight(words[i - 2], 17) ^ rotateRight(words[i - 2], 19) ^ (words[i - 2] >>> 10)
395
+
396
+ words[i] = add32(words[i - 16], s0, words[i - 7], s1)
397
+ }
398
+
399
+ let [a, b, c, d, e, f, g, h] = hash
400
+
401
+ for (let i = 0; i < 64; i++) {
402
+ const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25)
403
+ const ch = (e & f) ^ ((~e) & g)
404
+ const temp1 = add32(h, s1, ch, SHA256_K[i], words[i])
405
+ const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)
406
+ const maj = (a & b) ^ (a & c) ^ (b & c)
407
+ const temp2 = add32(s0, maj)
408
+
409
+ h = g
410
+ g = f
411
+ f = e
412
+ e = add32(d, temp1)
413
+ d = c
414
+ c = b
415
+ b = a
416
+ a = add32(temp1, temp2)
417
+ }
418
+
419
+ hash[0] = add32(hash[0], a)
420
+ hash[1] = add32(hash[1], b)
421
+ hash[2] = add32(hash[2], c)
422
+ hash[3] = add32(hash[3], d)
423
+ hash[4] = add32(hash[4], e)
424
+ hash[5] = add32(hash[5], f)
425
+ hash[6] = add32(hash[6], g)
426
+ hash[7] = add32(hash[7], h)
427
+ }
428
+
429
+ return hash.map((value) => value.toString(16).padStart(8, "0")).join("")
430
+ }
431
+
432
+ /**
433
+ * Converts a string to UTF-8 bytes.
434
+ * @param {string} value - String value.
435
+ * @returns {number[]} - UTF-8 bytes.
436
+ */
437
+ function utf8Bytes(value) {
438
+ /** @type {number[]} */
439
+ const bytes = []
440
+
441
+ for (const character of value) {
442
+ const codePoint = /** @type {number} */ (character.codePointAt(0))
443
+
444
+ if (codePoint <= 0x7f) {
445
+ bytes.push(codePoint)
446
+ } else if (codePoint <= 0x7ff) {
447
+ bytes.push(0xc0 | (codePoint >>> 6), 0x80 | (codePoint & 0x3f))
448
+ } else if (codePoint <= 0xffff) {
449
+ bytes.push(0xe0 | (codePoint >>> 12), 0x80 | ((codePoint >>> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
450
+ } else {
451
+ bytes.push(0xf0 | (codePoint >>> 18), 0x80 | ((codePoint >>> 12) & 0x3f), 0x80 | ((codePoint >>> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
452
+ }
453
+ }
454
+
455
+ return bytes
456
+ }
457
+
458
+ /**
459
+ * Adds unsigned 32-bit integers.
460
+ * @param {...number} values - Values to add.
461
+ * @returns {number} - Unsigned 32-bit result.
462
+ */
463
+ function add32(...values) {
464
+ return values.reduce((sum, value) => (sum + value) >>> 0, 0)
465
+ }
466
+
467
+ /**
468
+ * Rotates a 32-bit integer right.
469
+ * @param {number} value - Value to rotate.
470
+ * @param {number} bits - Bit count.
471
+ * @returns {number} - Rotated value.
472
+ */
473
+ function rotateRight(value, bits) {
474
+ return (value >>> bits) | (value << (32 - bits))
475
+ }
476
+
477
+ /**
478
+ * Validates that a sync config subtree is deterministic JSON and does not contain obvious secrets.
479
+ * @param {object} args - Arguments.
480
+ * @param {string} args.label - Diagnostic path label.
481
+ * @param {unknown} args.value - Value to validate.
482
+ * @returns {import("../configuration-types.js").FrontendModelSyncJsonValue} - Stable JSON value.
483
+ */
484
+ function deterministicSyncJson({label, value}) {
485
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value
486
+
487
+ if (Array.isArray(value)) {
488
+ return value.map((entry, index) => deterministicSyncJson({label: `${label}/${index}`, value: entry}))
489
+ }
490
+
491
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
492
+ /** @type {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue>} */
493
+ const normalized = {}
494
+
495
+ for (const key of Object.keys(value).sort()) {
496
+ const childValue = /** @type {Record<string, unknown>} */ (value)[key]
497
+
498
+ if (childValue === undefined) continue
499
+ if (syncConfigKeyLooksSecret(key)) {
500
+ throw new Error(`Sync policy ${label}/${key} is not allowed in frontend-visible sync policy config`)
501
+ }
502
+
503
+ normalized[key] = deterministicSyncJson({label: `${label}/${key}`, value: childValue})
504
+ }
505
+
506
+ return normalized
507
+ }
508
+
509
+ throw new Error("Sync policy input must be deterministic JSON")
510
+ }
511
+
512
+ /**
513
+ * Stable JSON stringifier with sorted object keys.
514
+ * @param {unknown} value - Value to stringify.
515
+ * @returns {string} - Stable JSON.
516
+ */
517
+ function stableJsonStringify(value) {
518
+ return JSON.stringify(deterministicSyncJson({label: "hash", value}))
519
+ }
520
+
521
+ /**
522
+ * Returns whether a sync config key looks like a credential/secret.
523
+ * @param {string} key - Object key.
524
+ * @returns {boolean} - Whether key is disallowed.
525
+ */
526
+ function syncConfigKeyLooksSecret(key) {
527
+ return /secret|token|password|private.?key|signing.?key/i.test(key)
528
+ }
529
+
153
530
  /**
154
531
  * Runs normalize frontend model resource commands.
155
532
  * @param {import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Raw resource configuration.
@@ -17,6 +17,14 @@ const FRONTEND_MODEL_CONTROLLER_PATH = new URL("../../frontend-model-controller.
17
17
  export default async function frontendModelCommandRouteHook({configuration, currentPath, hasMatchingCustomRoute}) {
18
18
  const normalizedCurrentPath = normalizePath(currentPath)
19
19
 
20
+ if (normalizedCurrentPath === "/frontend-models/sync/bootstrap") {
21
+ return {
22
+ action: "frontend-sync-bootstrap",
23
+ controller: "velocious/api",
24
+ controllerPath: FRONTEND_MODEL_CONTROLLER_PATH
25
+ }
26
+ }
27
+
20
28
  if (normalizedCurrentPath === SHARED_FRONTEND_MODEL_API_PATH) {
21
29
  return {
22
30
  action: "frontend-api",