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.
- package/README.md +7 -2
- package/build/authorization/base-resource.js +99 -0
- package/build/configuration-types.js +41 -3
- package/build/configuration.js +41 -1
- package/build/database/drivers/base.js +51 -3
- package/build/database/drivers/mysql/index.js +74 -7
- package/build/database/drivers/sqlite/connection-sql-js.js +16 -5
- package/build/database/drivers/sqlite/index.web.js +29 -15
- package/build/database/drivers/sqlite/web-persistence.js +521 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
- package/build/frontend-model-controller.js +139 -16
- package/build/frontend-model-resource/base-resource.js +258 -46
- package/build/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/build/frontend-models/base.js +146 -2
- package/build/frontend-models/query.js +3 -3
- package/build/frontend-models/resource-definition.js +400 -23
- package/build/routes/hooks/frontend-model-command-route-hook.js +8 -0
- package/build/src/authorization/base-resource.d.ts +41 -0
- package/build/src/authorization/base-resource.d.ts.map +1 -1
- package/build/src/authorization/base-resource.js +88 -1
- package/build/src/configuration-types.d.ts +128 -5
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +38 -4
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +37 -2
- package/build/src/database/drivers/base.d.ts +30 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +47 -4
- package/build/src/database/drivers/mysql/index.d.ts +32 -3
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +65 -7
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +8 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.js +15 -6
- package/build/src/database/drivers/sqlite/index.web.d.ts +5 -5
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.js +27 -13
- package/build/src/database/drivers/sqlite/web-persistence.d.ts +77 -0
- package/build/src/database/drivers/sqlite/web-persistence.d.ts.map +1 -0
- package/build/src/database/drivers/sqlite/web-persistence.js +464 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +30 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +54 -2
- package/build/src/frontend-model-controller.d.ts +34 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +125 -17
- package/build/src/frontend-model-resource/base-resource.d.ts +69 -0
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +247 -54
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +0 -2
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +1 -3
- package/build/src/frontend-models/base.d.ts +29 -0
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +131 -3
- package/build/src/frontend-models/query.js +4 -4
- package/build/src/frontend-models/resource-definition.d.ts +6 -0
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +355 -28
- package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
- package/build/src/routes/hooks/frontend-model-command-route-hook.js +8 -1
- package/build/src/sync/device-identity.d.ts +240 -0
- package/build/src/sync/device-identity.d.ts.map +1 -0
- package/build/src/sync/device-identity.js +454 -0
- package/build/src/sync/local-mutation-log.d.ts +212 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -0
- package/build/src/sync/local-mutation-log.js +404 -0
- package/build/src/sync/offline-grant.d.ts +157 -0
- package/build/src/sync/offline-grant.d.ts.map +1 -0
- package/build/src/sync/offline-grant.js +292 -0
- package/build/sync/device-identity.js +503 -0
- package/build/sync/local-mutation-log.js +448 -0
- package/build/sync/offline-grant.js +327 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/scripts/tensorbuzz-retry +21 -0
- package/src/authorization/base-resource.js +99 -0
- package/src/configuration-types.js +41 -3
- package/src/configuration.js +41 -1
- package/src/database/drivers/base.js +51 -3
- package/src/database/drivers/mysql/index.js +74 -7
- package/src/database/drivers/sqlite/connection-sql-js.js +16 -5
- package/src/database/drivers/sqlite/index.web.js +29 -15
- package/src/database/drivers/sqlite/web-persistence.js +521 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
- package/src/frontend-model-controller.js +139 -16
- package/src/frontend-model-resource/base-resource.js +258 -46
- package/src/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/src/frontend-models/base.js +146 -2
- package/src/frontend-models/query.js +3 -3
- package/src/frontend-models/resource-definition.js +400 -23
- package/src/routes/hooks/frontend-model-command-route-hook.js +8 -0
- package/src/sync/device-identity.js +503 -0
- package/src/sync/local-mutation-log.js +448 -0
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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",
|