@jini-ai/cms 0.3.4 → 0.3.6
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 +22 -17
- package/dist/content-types/agent-tools.d.ts.map +1 -1
- package/dist/content-types/agent-tools.js +1 -1
- package/dist/content-types/agent-tools.js.map +1 -1
- package/dist/content-types/errors.d.ts +19 -1
- package/dist/content-types/errors.d.ts.map +1 -1
- package/dist/content-types/errors.js +22 -1
- package/dist/content-types/errors.js.map +1 -1
- package/dist/content-types/index-provisioning.d.ts +56 -10
- package/dist/content-types/index-provisioning.d.ts.map +1 -1
- package/dist/content-types/index-provisioning.js +86 -13
- package/dist/content-types/index-provisioning.js.map +1 -1
- package/dist/content-types/index.d.ts +15 -3
- package/dist/content-types/index.d.ts.map +1 -1
- package/dist/content-types/index.js +14 -2
- package/dist/content-types/index.js.map +1 -1
- package/dist/content-types/lifecycle.js +3 -3
- package/dist/content-types/lifecycle.js.map +1 -1
- package/dist/content-types/types.d.ts +64 -3
- package/dist/content-types/types.d.ts.map +1 -1
- package/dist/content-types/types.js +67 -3
- package/dist/content-types/types.js.map +1 -1
- package/dist/content-types/write-service.d.ts +18 -3
- package/dist/content-types/write-service.d.ts.map +1 -1
- package/dist/content-types/write-service.js +28 -2
- package/dist/content-types/write-service.js.map +1 -1
- package/dist/core/ports.d.ts +12 -2
- package/dist/core/ports.d.ts.map +1 -1
- package/dist/entries/field-validation.d.ts.map +1 -1
- package/dist/entries/field-validation.js +96 -1
- package/dist/entries/field-validation.js.map +1 -1
- package/dist/entries/write-service.d.ts +14 -0
- package/dist/entries/write-service.d.ts.map +1 -1
- package/dist/entries/write-service.js +2 -2
- package/dist/entries/write-service.js.map +1 -1
- package/dist/identity/admin-crud-service.d.ts +28 -0
- package/dist/identity/admin-crud-service.d.ts.map +1 -1
- package/dist/identity/admin-crud-service.js +30 -0
- package/dist/identity/admin-crud-service.js.map +1 -1
- package/dist/identity/auth-service.d.ts +32 -1
- package/dist/identity/auth-service.d.ts.map +1 -1
- package/dist/identity/auth-service.js +44 -15
- package/dist/identity/auth-service.js.map +1 -1
- package/dist/identity/index.d.ts +4 -1
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +4 -1
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/seed.d.ts +14 -2
- package/dist/identity/seed.d.ts.map +1 -1
- package/dist/identity/seed.js +197 -45
- package/dist/identity/seed.js.map +1 -1
- package/dist/media/blob-store.fs.d.ts +7 -0
- package/dist/media/blob-store.fs.d.ts.map +1 -1
- package/dist/media/blob-store.fs.js +12 -2
- package/dist/media/blob-store.fs.js.map +1 -1
- package/dist/media/content-type-sniffer.d.ts +1 -1
- package/dist/media/content-type-sniffer.d.ts.map +1 -1
- package/dist/media/content-type-sniffer.js +52 -4
- package/dist/media/content-type-sniffer.js.map +1 -1
- package/dist/media/html-attributes.d.ts +90 -0
- package/dist/media/html-attributes.d.ts.map +1 -0
- package/dist/media/html-attributes.js +161 -0
- package/dist/media/html-attributes.js.map +1 -0
- package/dist/media/image-transformer.sharp.d.ts +35 -1
- package/dist/media/image-transformer.sharp.d.ts.map +1 -1
- package/dist/media/image-transformer.sharp.js +44 -2
- package/dist/media/image-transformer.sharp.js.map +1 -1
- package/dist/media/index.d.ts +2 -1
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +2 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/media-service.d.ts +93 -1
- package/dist/media/media-service.d.ts.map +1 -1
- package/dist/media/media-service.js +254 -7
- package/dist/media/media-service.js.map +1 -1
- package/dist/media/ports.d.ts +8 -1
- package/dist/media/ports.d.ts.map +1 -1
- package/dist/media/rendition-service.d.ts +7 -1
- package/dist/media/rendition-service.d.ts.map +1 -1
- package/dist/media/rendition-service.js +10 -4
- package/dist/media/rendition-service.js.map +1 -1
- package/dist/media/repo.memory.d.ts +4 -0
- package/dist/media/repo.memory.d.ts.map +1 -1
- package/dist/media/repo.memory.js +3 -0
- package/dist/media/repo.memory.js.map +1 -1
- package/dist/media/tool-registrations.d.ts +6 -2
- package/dist/media/tool-registrations.d.ts.map +1 -1
- package/dist/media/tool-registrations.js +19 -2
- package/dist/media/tool-registrations.js.map +1 -1
- package/dist/media/types.d.ts +26 -0
- package/dist/media/types.d.ts.map +1 -1
- package/dist/media/types.js.map +1 -1
- package/dist/navigation/agent-tools.d.ts.map +1 -1
- package/dist/navigation/agent-tools.js +8 -1
- package/dist/navigation/agent-tools.js.map +1 -1
- package/dist/navigation/index.d.ts +10 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +10 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/menu-service.d.ts +60 -4
- package/dist/navigation/menu-service.d.ts.map +1 -1
- package/dist/navigation/menu-service.js +106 -8
- package/dist/navigation/menu-service.js.map +1 -1
- package/package.json +1 -1
package/dist/identity/seed.js
CHANGED
|
@@ -23,6 +23,16 @@ import { normalizeUsername } from "./username.js";
|
|
|
23
23
|
*/
|
|
24
24
|
/** REQ-09: the literal id the legacy Article VI actor stamp resolves to. */
|
|
25
25
|
const LEGACY_USER_LOCAL_PRINCIPAL_ID = "user-local";
|
|
26
|
+
/**
|
|
27
|
+
* The built-in role the seeded owner user holds, and its authority. Named constants because two
|
|
28
|
+
* call sites now depend on them agreeing exactly — the fresh seed below and `ensureOwnerRoleBinding`,
|
|
29
|
+
* which repairs an interrupted one. Drift between the two would be a silent authority bug.
|
|
30
|
+
*
|
|
31
|
+
* REQ-04/REQ-09: the owner policy holds the wildcard `*`, not an enumerated list, so it
|
|
32
|
+
* automatically covers permissions features register later.
|
|
33
|
+
*/
|
|
34
|
+
const OWNER_ROLE_NAME = "owner";
|
|
35
|
+
const OWNER_ROLE_PERMISSIONS = ["*"];
|
|
26
36
|
const BUILTIN_ADMIN_PERMISSIONS = [
|
|
27
37
|
"content.read",
|
|
28
38
|
"content.write",
|
|
@@ -136,22 +146,77 @@ const BUILTIN_EDITOR_PERMISSIONS = [
|
|
|
136
146
|
"theme.set",
|
|
137
147
|
];
|
|
138
148
|
const BUILTIN_VIEWER_PERMISSIONS = ["content.read", "changeset.read", "plugin.read"];
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Why every step below is "find, then save only what is missing" rather than a straight insert.
|
|
151
|
+
*
|
|
152
|
+
* This function's writes are NOT transactional and its caller is not awaited by its hosts (Tovu
|
|
153
|
+
* hands `seedIdentity`'s promise back as `RouteDeps.identityReady` and returns synchronously), so
|
|
154
|
+
* a process that exits before the seed finishes — a fast-failing CLI command, a Ctrl-C, a crash —
|
|
155
|
+
* leaves the workspace half-seeded. `seedIdentity`'s own early-return guard keys on the OWNER USER,
|
|
156
|
+
* which is written last, so the next boot correctly decides the seed is unfinished and replays it.
|
|
157
|
+
* A replay that blindly re-inserted would hit `UNIQUE(workspace_id, name)` on `roles`/`policies`
|
|
158
|
+
* (crashing every subsequent boot, permanently), and would silently duplicate `role_policies` /
|
|
159
|
+
* `policy_permissions` rows, which carry no unique index at all. So the replay must converge onto
|
|
160
|
+
* the rows already there instead of re-creating them.
|
|
161
|
+
*
|
|
162
|
+
* What this deliberately does NOT do: adopt a role or policy an operator created themselves. A
|
|
163
|
+
* same-named non-built-in row is a genuine conflict, not a partial seed, and silently promoting it
|
|
164
|
+
* would hand its holders the built-in policy's permissions. Those cases throw.
|
|
165
|
+
*/
|
|
166
|
+
/** The built-in role for `name`, reusing a prior (possibly interrupted) seed's row if present. */
|
|
167
|
+
async function ensureBuiltinRole(required) {
|
|
168
|
+
const { deps, workspaceId, name } = required;
|
|
169
|
+
const existing = await deps.repos.roles.findByName({ workspaceId, name });
|
|
170
|
+
if (!existing) {
|
|
171
|
+
const roleId = deps.idGen.newId();
|
|
172
|
+
await deps.repos.roles.save({ id: roleId, workspaceId, name, isBuiltin: true });
|
|
173
|
+
return roleId;
|
|
174
|
+
}
|
|
175
|
+
if (!existing.isBuiltin) {
|
|
176
|
+
throw new Error(`seedIdentity: a non-built-in role named '${name}' already exists in workspace ${workspaceId}. ` +
|
|
177
|
+
`Refusing to adopt it as the built-in '${name}' role — rename or remove it first.`);
|
|
178
|
+
}
|
|
179
|
+
return existing.id;
|
|
180
|
+
}
|
|
181
|
+
/** The built-in policy for `name`, reusing a prior (possibly interrupted) seed's row if present. */
|
|
182
|
+
async function ensureBuiltinPolicy(required) {
|
|
183
|
+
const { deps, workspaceId, name } = required;
|
|
184
|
+
const policyName = `${name}-builtin-policy`;
|
|
185
|
+
const existing = await deps.repos.policies.findByName({ workspaceId, name: policyName });
|
|
186
|
+
if (!existing) {
|
|
187
|
+
const policyId = deps.idGen.newId();
|
|
188
|
+
await deps.repos.policies.save({
|
|
189
|
+
id: policyId,
|
|
190
|
+
workspaceId,
|
|
191
|
+
name: policyName,
|
|
192
|
+
description: `Built-in policy for the seeded '${name}' role.`,
|
|
193
|
+
isBuiltin: true,
|
|
194
|
+
isFrozen: false,
|
|
195
|
+
});
|
|
196
|
+
return policyId;
|
|
197
|
+
}
|
|
198
|
+
if (!existing.isBuiltin) {
|
|
199
|
+
throw new Error(`seedIdentity: a non-built-in policy named '${policyName}' already exists in workspace ${workspaceId}. ` +
|
|
200
|
+
`Refusing to adopt it as the built-in '${name}' policy — rename or remove it first.`);
|
|
201
|
+
}
|
|
202
|
+
return existing.id;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Grant `permissions` to `policyId`, skipping any the policy already holds.
|
|
206
|
+
*
|
|
207
|
+
* Additive by design, and the reason a resumed seed still CONVERGES rather than merely staying
|
|
208
|
+
* quiet: a built-in policy whose permission rows never landed (the interrupt fell between the
|
|
209
|
+
* policy insert and this loop) gains them on the next boot. Compares only unscoped rows because
|
|
210
|
+
* that is the only shape this seed writes — a `resourceType`-scoped row of the same name is a
|
|
211
|
+
* different grant and must not satisfy an unscoped one.
|
|
212
|
+
*/
|
|
213
|
+
async function ensurePolicyPermissions(required) {
|
|
214
|
+
const { deps, workspaceId, policyId, permissions } = required;
|
|
215
|
+
const existing = await deps.repos.policyPermissions.listByPolicyId({ workspaceId, policyId });
|
|
216
|
+
const held = new Set(existing.filter((row) => row.resourceType === null).map((row) => row.permission));
|
|
154
217
|
for (const permission of permissions) {
|
|
218
|
+
if (held.has(permission))
|
|
219
|
+
continue;
|
|
155
220
|
await deps.repos.policyPermissions.save({
|
|
156
221
|
id: deps.idGen.newId(),
|
|
157
222
|
workspaceId,
|
|
@@ -161,12 +226,84 @@ async function seedBuiltinRoleWithPolicy(required) {
|
|
|
161
226
|
constraintJson: null,
|
|
162
227
|
});
|
|
163
228
|
}
|
|
229
|
+
}
|
|
230
|
+
/** Builds one built-in role + its 1:1 policy + policy_permissions rows, converging on what exists. */
|
|
231
|
+
async function seedBuiltinRoleWithPolicy(required) {
|
|
232
|
+
const { deps, workspaceId, name, permissions, nowIso } = required;
|
|
233
|
+
const roleId = await ensureBuiltinRole({ deps, workspaceId, name });
|
|
234
|
+
const policyId = await ensureBuiltinPolicy({ deps, workspaceId, name });
|
|
235
|
+
// `role_policies` has no unique index, so a replay would otherwise stack a second identical link.
|
|
236
|
+
const links = await deps.repos.rolePolicies.listByRoleId({ workspaceId, roleId });
|
|
237
|
+
if (!links.some((link) => link.policyId === policyId)) {
|
|
238
|
+
await deps.repos.rolePolicies.save({ id: deps.idGen.newId(), workspaceId, roleId, policyId });
|
|
239
|
+
}
|
|
240
|
+
await ensurePolicyPermissions({ deps, workspaceId, policyId, permissions });
|
|
164
241
|
void nowIso; // reserved for a future createdAt column on roles/policies
|
|
165
242
|
return { roleId, policyId };
|
|
166
243
|
}
|
|
167
244
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
245
|
+
* Repair the one completion step `seedIdentity`'s owner-user guard cannot see.
|
|
246
|
+
*
|
|
247
|
+
* The guard keys on the owner USER, and the owner's `principal_roles` link is written after it. A
|
|
248
|
+
* process that exits between those two adjacent writes leaves a workspace whose owner exists, can
|
|
249
|
+
* still log in, and holds no role — and `resolveEffectivePermissions` (`authorize.ts`) starts from
|
|
250
|
+
* `principal_roles` + `principal_policies`, so with neither present EVERY permission evaluates to
|
|
251
|
+
* `no_grant`. Without this step the guard early-returns on the user it finds forever and the site
|
|
252
|
+
* stays bricked for its owner, silently. Neither host-side reconciler closes it either: Tovu's
|
|
253
|
+
* `migrateDeprecatedPermissionGrants` and `applyBuiltinRoleGrants` are handed policy and role repos
|
|
254
|
+
* only, never `principalRoles`, so neither can write a principal->role link at all.
|
|
255
|
+
*
|
|
256
|
+
* Why the gate is "holds NO role" rather than "lacks the owner role". Those two conditions differ
|
|
257
|
+
* on exactly one workspace shape, and it matters: an operator who moves the seeded owner principal
|
|
258
|
+
* to a lesser role leaves it lacking the owner link on purpose. Repairing on that broader signal
|
|
259
|
+
* would re-escalate the principal to wildcard authority on the next restart — a privilege
|
|
260
|
+
* escalation shipped as a bug fix. A principal holding no role at all is never a deliberate state;
|
|
261
|
+
* it is only ever the interrupted seed, so that is what this repairs.
|
|
262
|
+
*
|
|
263
|
+
* Converges rather than assuming: it goes through `seedBuiltinRoleWithPolicy`, so a workspace whose
|
|
264
|
+
* owner ROLE or wildcard grant also never landed gains those here instead of being linked to a role
|
|
265
|
+
* that grants nothing. `principal_roles` carries no unique index, hence the read-before-write.
|
|
266
|
+
*
|
|
267
|
+
* @complexity O(1) — a handful of point reads on a healthy boot (one `listByPrincipalId`, which
|
|
268
|
+
* returns early), plus the fixed converge pass for the owner role only when the repair fires.
|
|
269
|
+
*/
|
|
270
|
+
async function ensureOwnerRoleBinding(required) {
|
|
271
|
+
const { deps, workspaceId, ownerPrincipalId, nowIso } = required;
|
|
272
|
+
const heldRoles = await deps.repos.principalRoles.listByPrincipalId({
|
|
273
|
+
workspaceId,
|
|
274
|
+
principalId: ownerPrincipalId,
|
|
275
|
+
});
|
|
276
|
+
if (heldRoles.length > 0)
|
|
277
|
+
return;
|
|
278
|
+
const { roleId } = await seedBuiltinRoleWithPolicy({
|
|
279
|
+
deps,
|
|
280
|
+
workspaceId,
|
|
281
|
+
name: OWNER_ROLE_NAME,
|
|
282
|
+
permissions: OWNER_ROLE_PERMISSIONS,
|
|
283
|
+
nowIso,
|
|
284
|
+
});
|
|
285
|
+
await deps.repos.principalRoles.save({
|
|
286
|
+
id: deps.idGen.newId(),
|
|
287
|
+
workspaceId,
|
|
288
|
+
principalId: ownerPrincipalId,
|
|
289
|
+
roleId,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Seed first-boot identity data.
|
|
294
|
+
*
|
|
295
|
+
* Idempotent in two distinct senses, and both are load-bearing:
|
|
296
|
+
* - A seed that RAN TO COMPLETION is a no-op — the owner-username lookup below early-returns after
|
|
297
|
+
* a single confirming read (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
298
|
+
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
299
|
+
* Tovu's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
300
|
+
* `identityReady`.)
|
|
301
|
+
* - "Ran to completion" is judged on the owner user AND its role link, not the user alone. The link
|
|
302
|
+
* is the seed's last write, so the user is not on its own evidence the seed finished — see
|
|
303
|
+
* `ensureOwnerRoleBinding` for what an interrupt between those two writes costs.
|
|
304
|
+
* - A seed that was INTERRUPTED is resumed, not replayed: every step converges onto the rows a
|
|
305
|
+
* previous attempt already wrote and fills in only what is missing. See the block comment above
|
|
306
|
+
* `ensureBuiltinRole` for why this function must survive being killed halfway.
|
|
170
307
|
*
|
|
171
308
|
* @complexity O(1) — fixed small number of inserts (4 roles/policies, ~2
|
|
172
309
|
* dozen policy_permissions, 3 principals, 1 user, 1 role assignment).
|
|
@@ -183,21 +320,35 @@ export async function seedIdentity(required) {
|
|
|
183
320
|
username: ownerUsername,
|
|
184
321
|
});
|
|
185
322
|
if (existingOwnerUser) {
|
|
323
|
+
// The owner user alone is NOT proof the seed finished — its role link is written after it. See
|
|
324
|
+
// `ensureOwnerRoleBinding`: on a healthy workspace this is one read and no write.
|
|
325
|
+
await ensureOwnerRoleBinding({
|
|
326
|
+
deps,
|
|
327
|
+
workspaceId,
|
|
328
|
+
ownerPrincipalId: existingOwnerUser.principalId,
|
|
329
|
+
nowIso,
|
|
330
|
+
});
|
|
186
331
|
const existingSystem = (await deps.repos.principals.list({ workspaceId })).find((row) => row.kind === "system" && row.id !== LEGACY_USER_LOCAL_PRINCIPAL_ID);
|
|
187
332
|
return {
|
|
188
333
|
ownerPrincipalId: existingOwnerUser.principalId,
|
|
189
334
|
systemPrincipalId: existingSystem?.id ?? LEGACY_USER_LOCAL_PRINCIPAL_ID,
|
|
190
335
|
};
|
|
191
336
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
337
|
+
// Reuse the system principal a prior interrupted seed may already have written: `principals` has
|
|
338
|
+
// no unique index on kind, so a blind re-insert would leave TWO non-legacy system principals and
|
|
339
|
+
// the early-return branch above picking between them by list order.
|
|
340
|
+
const existingSystem = (await deps.repos.principals.list({ workspaceId })).find((row) => row.kind === "system" && row.id !== LEGACY_USER_LOCAL_PRINCIPAL_ID);
|
|
341
|
+
const systemPrincipalId = existingSystem?.id ?? deps.idGen.newId();
|
|
342
|
+
if (!existingSystem) {
|
|
343
|
+
await deps.repos.principals.save({
|
|
344
|
+
id: systemPrincipalId,
|
|
345
|
+
workspaceId,
|
|
346
|
+
kind: "system",
|
|
347
|
+
displayName: "System",
|
|
348
|
+
status: "active",
|
|
349
|
+
createdAt: nowIso,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
201
352
|
// REQ-09/EC-09: disabled legacy actor so historical `actorId='user-local'`
|
|
202
353
|
// change-sets resolve without rewriting history.
|
|
203
354
|
await deps.repos.principals.save({
|
|
@@ -212,23 +363,10 @@ export async function seedIdentity(required) {
|
|
|
212
363
|
const { roleId: ownerRoleId } = await seedBuiltinRoleWithPolicy({
|
|
213
364
|
deps,
|
|
214
365
|
workspaceId,
|
|
215
|
-
name:
|
|
216
|
-
permissions:
|
|
366
|
+
name: OWNER_ROLE_NAME,
|
|
367
|
+
permissions: OWNER_ROLE_PERMISSIONS,
|
|
217
368
|
nowIso,
|
|
218
369
|
});
|
|
219
|
-
// REQ-04/REQ-09: the owner policy holds the wildcard `*`, not an enumerated
|
|
220
|
-
// list, so it automatically covers permissions features register later.
|
|
221
|
-
const ownerPolicy = (await deps.repos.policies.list({ workspaceId })).find((row) => row.name === "owner-builtin-policy");
|
|
222
|
-
if (!ownerPolicy)
|
|
223
|
-
throw new Error("seedIdentity: owner policy was not created");
|
|
224
|
-
await deps.repos.policyPermissions.save({
|
|
225
|
-
id: deps.idGen.newId(),
|
|
226
|
-
workspaceId,
|
|
227
|
-
policyId: ownerPolicy.id,
|
|
228
|
-
permission: "*",
|
|
229
|
-
resourceType: null,
|
|
230
|
-
constraintJson: null,
|
|
231
|
-
});
|
|
232
370
|
await seedBuiltinRoleWithPolicy({
|
|
233
371
|
deps,
|
|
234
372
|
workspaceId,
|
|
@@ -250,6 +388,12 @@ export async function seedIdentity(required) {
|
|
|
250
388
|
permissions: BUILTIN_VIEWER_PERMISSIONS,
|
|
251
389
|
nowIso,
|
|
252
390
|
});
|
|
391
|
+
// Hashed BEFORE the owner principal row is written, not inline in the `users.save` below.
|
|
392
|
+
// argon2id is deliberately slow, which made it by far the widest window in this un-awaited,
|
|
393
|
+
// non-transactional sequence for a process to exit between the principal insert and the user
|
|
394
|
+
// insert that gives it meaning — leaving an orphan `kind: "user"` principal behind on every
|
|
395
|
+
// interrupted boot. Hashing first shrinks that window to a single adjacent await.
|
|
396
|
+
const ownerPasswordHash = await deps.hasher.hash(ownerPassword);
|
|
253
397
|
const ownerPrincipalId = deps.idGen.newId();
|
|
254
398
|
await deps.repos.principals.save({
|
|
255
399
|
id: ownerPrincipalId,
|
|
@@ -264,14 +408,22 @@ export async function seedIdentity(required) {
|
|
|
264
408
|
workspaceId,
|
|
265
409
|
username: ownerUsername,
|
|
266
410
|
email: input.ownerEmail,
|
|
267
|
-
passwordHash:
|
|
411
|
+
passwordHash: ownerPasswordHash,
|
|
268
412
|
});
|
|
269
|
-
|
|
270
|
-
|
|
413
|
+
// `principal_roles` carries no unique index either — same converge-don't-duplicate rule as the
|
|
414
|
+
// `role_policies` link in `seedBuiltinRoleWithPolicy`.
|
|
415
|
+
const ownerRoleLinks = await deps.repos.principalRoles.listByPrincipalId({
|
|
271
416
|
workspaceId,
|
|
272
417
|
principalId: ownerPrincipalId,
|
|
273
|
-
roleId: ownerRoleId,
|
|
274
418
|
});
|
|
419
|
+
if (!ownerRoleLinks.some((link) => link.roleId === ownerRoleId)) {
|
|
420
|
+
await deps.repos.principalRoles.save({
|
|
421
|
+
id: deps.idGen.newId(),
|
|
422
|
+
workspaceId,
|
|
423
|
+
principalId: ownerPrincipalId,
|
|
424
|
+
roleId: ownerRoleId,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
275
427
|
return { ownerPrincipalId, systemPrincipalId };
|
|
276
428
|
}
|
|
277
429
|
//# sourceMappingURL=seed.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../src/identity/seed.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,4EAA4E;AAC5E,MAAM,8BAA8B,GAAG,YAAY,CAAC;AAEpD,MAAM,yBAAyB,GAAsB;IACnD,cAAc;IACd,eAAe;IACf,iBAAiB;IACjB,gBAAgB;IAChB,WAAW;IACX,4FAA4F;IAC5F,6FAA6F;IAC7F,iGAAiG;IACjG,sCAAsC;IACtC,YAAY;IACZ,aAAa;IACb,eAAe;IACf,gBAAgB;IAChB,gBAAgB;IAChB,kBAAkB;IAClB,eAAe;IACf,gBAAgB;IAChB,eAAe;IACf,wEAAwE;IACxE,kGAAkG;IAClG,6FAA6F;IAC7F,2FAA2F;IAC3F,4FAA4F;IAC5F,sDAAsD;IACtD,YAAY;IACZ,cAAc;IACd,cAAc;IACd,cAAc;IACd,oBAAoB;IACpB,yBAAyB;IACzB,kBAAkB;IAClB,2DAA2D;IAC3D,uFAAuF;IACvF,uFAAuF;IACvF,2FAA2F;IAC3F,gGAAgG;IAChG,8FAA8F;IAC9F,2FAA2F;IAC3F,+FAA+F;IAC/F,4BAA4B;IAC5B,kBAAkB;IAClB,oBAAoB;IACpB,oBAAoB;IACpB,oBAAoB;IACpB,0BAA0B;IAC1B,oBAAoB;IACpB,oEAAoE;IACpE,8FAA8F;IAC9F,+FAA+F;IAC/F,yFAAyF;IACzF,2BAA2B;IAC3B,qDAAqD;IACrD,iEAAiE;IACjE,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,6BAA6B;IAC7B,uBAAuB;IACvB,0BAA0B;IAC1B,0BAA0B;IAC1B,qBAAqB;IACrB,wFAAwF;IACxF,0FAA0F;IAC1F,+FAA+F;IAC/F,6FAA6F;IAC7F,wFAAwF;IACxF,wDAAwD;IACxD,oBAAoB;IACpB,uBAAuB;IACvB,0BAA0B;IAC1B,qBAAqB;IACrB,eAAe;IACf,mBAAmB;IACnB,yBAAyB;IACzB,2BAA2B;IAC3B,uDAAuD;IACvD,8FAA8F;IAC9F,kBAAkB;IAClB,4FAA4F;IAC5F,iGAAiG;IACjG,kGAAkG;IAClG,kFAAkF;IAClF,uFAAuF;IACvF,wBAAwB;IACxB,wDAAwD;IACxD,+FAA+F;IAC/F,sBAAsB;IACtB,kBAAkB;IAClB,+EAA+E;CAChF,CAAC;AAEF,MAAM,0BAA0B,GAAsB;IACpD,cAAc;IACd,eAAe;IACf,iBAAiB;IACjB,gBAAgB;IAChB,mEAAmE;IACnE,0FAA0F;IAC1F,qFAAqF;IACrF,yFAAyF;IACzF,4EAA4E;IAC5E,gGAAgG;IAChG,iFAAiF;IACjF,gGAAgG;IAChG,2BAA2B;IAC3B,YAAY;IACZ,cAAc;IACd,cAAc;IACd,cAAc;IACd,WAAW;CACZ,CAAC;AAEF,MAAM,0BAA0B,GAAsB,CAAC,cAAc,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAC;AAgCxG,
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../src/identity/seed.ts"],"names":[],"mappings":"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"}
|
|
@@ -28,6 +28,13 @@ export declare class LocalFsBlobStore implements BlobStorePort {
|
|
|
28
28
|
get(input: {
|
|
29
29
|
storageKey: string;
|
|
30
30
|
}): Promise<Uint8Array>;
|
|
31
|
+
/**
|
|
32
|
+
* `ENOENT` (the path genuinely does not exist) resolves to `false`. Any other `stat()` failure
|
|
33
|
+
* (`EACCES` permission denied, `ENOTDIR`, `EIO`, ...) is rethrown raw — same "catch the one
|
|
34
|
+
* known code, rethrow everything else" shape {@link putIfAbsent}'s `EEXIST` handling above
|
|
35
|
+
* already uses — instead of collapsing into `false`: a caller branching on `exists()` must not
|
|
36
|
+
* mistake "this stat failed" for "the object was deleted".
|
|
37
|
+
*/
|
|
31
38
|
exists(input: {
|
|
32
39
|
storageKey: string;
|
|
33
40
|
}): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob-store.fs.d.ts","sourceRoot":"","sources":["../../src/media/blob-store.fs.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG9D,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,gBAAiB,YAAW,aAAa;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,IAAI,EAAE,oBAAoB;IAItC,OAAO,CAAC,WAAW;IAIb,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAQ/D;;;;;;;;;;OAUG;IACG,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAenF,GAAG,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"blob-store.fs.d.ts","sourceRoot":"","sources":["../../src/media/blob-store.fs.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG9D,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,gBAAiB,YAAW,aAAa;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,IAAI,EAAE,oBAAoB;IAItC,OAAO,CAAC,WAAW;IAIb,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAQ/D;;;;;;;;;;OAUG;IACG,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAenF,GAAG,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAI7D;;;;;;OAMG;IACG,MAAM,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAYvD,MAAM,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAG3D"}
|
|
@@ -58,13 +58,23 @@ export class LocalFsBlobStore {
|
|
|
58
58
|
async get(input) {
|
|
59
59
|
return readFile(this.resolvePath(input.storageKey));
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* `ENOENT` (the path genuinely does not exist) resolves to `false`. Any other `stat()` failure
|
|
63
|
+
* (`EACCES` permission denied, `ENOTDIR`, `EIO`, ...) is rethrown raw — same "catch the one
|
|
64
|
+
* known code, rethrow everything else" shape {@link putIfAbsent}'s `EEXIST` handling above
|
|
65
|
+
* already uses — instead of collapsing into `false`: a caller branching on `exists()` must not
|
|
66
|
+
* mistake "this stat failed" for "the object was deleted".
|
|
67
|
+
*/
|
|
61
68
|
async exists(input) {
|
|
62
69
|
try {
|
|
63
70
|
await stat(this.resolvePath(input.storageKey));
|
|
64
71
|
return true;
|
|
65
72
|
}
|
|
66
|
-
catch {
|
|
67
|
-
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err.code === "ENOENT") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
async remove(input) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob-store.fs.js","sourceRoot":"","sources":["../../src/media/blob-store.fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAOtD,MAAM,OAAO,gBAAgB;IACV,OAAO,CAAS;IAEjC,YAAY,IAA0B;QACpC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC9B,CAAC;IAEO,WAAW,CAAC,UAAkB;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAmB;QAC3B,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,EAAE,UAAU,EAAE,CAAC;IACxB,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,WAAW,CAAC,KAAmB;QACnC,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACxC,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAA6B;QACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAA6B;QACxC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,
|
|
1
|
+
{"version":3,"file":"blob-store.fs.js","sourceRoot":"","sources":["../../src/media/blob-store.fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAOtD,MAAM,OAAO,gBAAgB;IACV,OAAO,CAAS;IAEjC,YAAY,IAA0B;QACpC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC9B,CAAC;IAEO,WAAW,CAAC,UAAkB;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAmB;QAC3B,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,EAAE,UAAU,EAAE,CAAC;IACxB,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,WAAW,CAAC,KAAmB;QACnC,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACxC,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAA6B;QACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IACtD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,MAAM,CAAC,KAA6B;QACxC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,OAAO,KAAK,CAAC;YACf,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAA6B;QACxC,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;CACF"}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
/** Recognized outputs. Anything not covered here falls back to the network's
|
|
33
33
|
* default-safe unknown-binary label — see {@link sniffContentType}'s doc. */
|
|
34
|
-
export type SniffedContentType = "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "video/mp4" | "video/webm" | "text/html" | "image/svg+xml" | "application/octet-stream";
|
|
34
|
+
export type SniffedContentType = "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/avif" | "video/mp4" | "video/webm" | "text/html" | "image/svg+xml" | "application/octet-stream";
|
|
35
35
|
/**
|
|
36
36
|
* Identifies a media type from `bytes`' leading magic bytes. Allowlist-shaped: only the formats
|
|
37
37
|
* named in {@link SniffedContentType} are ever returned as something other than
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-type-sniffer.d.ts","sourceRoot":"","sources":["../../src/media/content-type-sniffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;6EAC6E;AAC7E,MAAM,MAAM,kBAAkB,GAC1B,WAAW,GACX,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,WAAW,GACX,eAAe,GACf,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"content-type-sniffer.d.ts","sourceRoot":"","sources":["../../src/media/content-type-sniffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;6EAC6E;AAC7E,MAAM,MAAM,kBAAkB,GAC1B,WAAW,GACX,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,WAAW,GACX,eAAe,GACf,0BAA0B,CAAC;AAwI/B;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,kBAAkB,CAgBtE"}
|
|
@@ -64,11 +64,55 @@ function isGif(bytes) {
|
|
|
64
64
|
function isWebp(bytes) {
|
|
65
65
|
return asciiEqualAt(bytes, 0, "RIFF") && asciiEqualAt(bytes, 8, "WEBP");
|
|
66
66
|
}
|
|
67
|
-
/** ISO-BMFF
|
|
68
|
-
*
|
|
69
|
-
|
|
67
|
+
/** ISO-BMFF: a leading box whose 4-byte type tag at offset 4 is `ftyp`. The box's own size field
|
|
68
|
+
* (offset 0-3) is deliberately not checked here — it varies by file. This says only "some member
|
|
69
|
+
* of the ISO-BMFF family", NOT "MP4": AVIF, HEIC and MP4 all match it, which is why
|
|
70
|
+
* {@link isAvif} must run first and why {@link sniffContentType} treats this as the family's
|
|
71
|
+
* last-resort fallback rather than a positive MP4 identification. */
|
|
72
|
+
function isIsoBmffFtyp(bytes) {
|
|
70
73
|
return asciiEqualAt(bytes, 4, "ftyp");
|
|
71
74
|
}
|
|
75
|
+
/** ISO-BMFF brands that identify AVIF: a still image (`avif`) or an image sequence (`avis`). */
|
|
76
|
+
const AVIF_BRANDS = ["avif", "avis"];
|
|
77
|
+
const FTYP_MAJOR_BRAND_OFFSET = 8;
|
|
78
|
+
/** Compatible brands follow the major brand (offset 8) and 4-byte minor version (offset 12). */
|
|
79
|
+
const FTYP_COMPATIBLE_BRANDS_OFFSET = 16;
|
|
80
|
+
/** Bound on the compatible-brand list scan — real `ftyp` boxes carry a handful, never dozens. */
|
|
81
|
+
const MAX_SCANNED_COMPATIBLE_BRANDS = 16;
|
|
82
|
+
/** The `ftyp` box's declared end offset (big-endian size at bytes 0-3), clamped to the bytes
|
|
83
|
+
* actually present so a truncated or size-inflating file can never push the brand scan past the
|
|
84
|
+
* buffer or into unrelated trailing boxes. */
|
|
85
|
+
function ftypBoxEnd(bytes) {
|
|
86
|
+
if (bytes.length < FTYP_MAJOR_BRAND_OFFSET)
|
|
87
|
+
return 0;
|
|
88
|
+
const declared = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0;
|
|
89
|
+
return Math.min(declared, bytes.length);
|
|
90
|
+
}
|
|
91
|
+
function hasAvifBrandAt(bytes, offset) {
|
|
92
|
+
return AVIF_BRANDS.some((brand) => asciiEqualAt(bytes, offset, brand));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* AVIF, distinguished from its ISO-BMFF siblings by brand rather than by the shared `ftyp` tag.
|
|
96
|
+
* The major brand alone is not sufficient: a great many real AVIF files declare the generic
|
|
97
|
+
* `mif1` (HEIF image) major brand and name `avif` only in the compatible-brand list, so both are
|
|
98
|
+
* checked. HEIC declares neither and is therefore never matched here — deliberate, since the
|
|
99
|
+
* installed libvips decodes AVIF but not HEIC.
|
|
100
|
+
*/
|
|
101
|
+
function isAvif(bytes) {
|
|
102
|
+
if (!isIsoBmffFtyp(bytes))
|
|
103
|
+
return false;
|
|
104
|
+
if (hasAvifBrandAt(bytes, FTYP_MAJOR_BRAND_OFFSET))
|
|
105
|
+
return true;
|
|
106
|
+
const boxEnd = ftypBoxEnd(bytes);
|
|
107
|
+
for (let index = 0; index < MAX_SCANNED_COMPATIBLE_BRANDS; index++) {
|
|
108
|
+
const offset = FTYP_COMPATIBLE_BRANDS_OFFSET + index * 4;
|
|
109
|
+
if (offset + 4 > boxEnd)
|
|
110
|
+
return false;
|
|
111
|
+
if (hasAvifBrandAt(bytes, offset))
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
72
116
|
function isWebm(bytes) {
|
|
73
117
|
return bytesEqualAt(bytes, 0, EBML_SIGNATURE);
|
|
74
118
|
}
|
|
@@ -127,7 +171,11 @@ export function sniffContentType(bytes) {
|
|
|
127
171
|
return "image/gif";
|
|
128
172
|
if (isWebp(bytes))
|
|
129
173
|
return "image/webp";
|
|
130
|
-
|
|
174
|
+
// MUST precede the MP4 check: AVIF is ISO-BMFF too, so the brand-blind `ftyp` test below would
|
|
175
|
+
// otherwise claim every AVIF as video/mp4 (the 2026-09-06 regression).
|
|
176
|
+
if (isAvif(bytes))
|
|
177
|
+
return "image/avif";
|
|
178
|
+
if (isIsoBmffFtyp(bytes))
|
|
131
179
|
return "video/mp4";
|
|
132
180
|
if (isWebm(bytes))
|
|
133
181
|
return "video/webm";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-type-sniffer.js","sourceRoot":"","sources":["../../src/media/content-type-sniffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;
|
|
1
|
+
{"version":3,"file":"content-type-sniffer.js","sourceRoot":"","sources":["../../src/media/content-type-sniffer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Server-side allowlist for `MediaRecord.htmlAttributes` (2026-09-07) — the enforcement half
|
|
3
|
+
* of a validator that ALSO has a browser-side copy in Tovu's admin
|
|
4
|
+
* (`apps/admin/src/features/media/rules.ts`, `parseMediaHtmlAttributes` and friends).
|
|
5
|
+
*
|
|
6
|
+
* Deliberately duplicated, not shared via one imported module, for the same reason this codebase
|
|
7
|
+
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into Tovu's `FILE_HANDLER_ALLOWED_MIME_TYPES`/
|
|
8
|
+
* `IMPORTABLE_CONTENT_TYPES`: `@jini-ai/cms/media`'s barrel (`index.ts`) re-exports files that
|
|
9
|
+
* import real Node built-ins (`node:crypto` in `media-service.ts`, `node:fs` in
|
|
10
|
+
* `blob-store.fs.ts`, native `sharp` bindings in `image-transformer.sharp.ts`) — importing this
|
|
11
|
+
* subpath from a browser-bundled Vite app (Tovu's `apps/admin`) risks pulling those into the admin
|
|
12
|
+
* SPA's bundle even if only this one pure function is actually used, and this admin's own copy
|
|
13
|
+
* already exists, tested, with no such risk. This file is the copy every Node-side consumer
|
|
14
|
+
* (`media-service.ts`'s own `updateMediaMetadata`, and Tovu's `apps/website` render path, both pure
|
|
15
|
+
* backend code) can safely import from `@jini-ai/cms/media` instead of re-deriving the rules a
|
|
16
|
+
* third time.
|
|
17
|
+
*
|
|
18
|
+
* THE RULES THEMSELVES ARE NOT REDESIGNED HERE — this is a verbatim port of the admin copy's logic
|
|
19
|
+
* (allowlist membership, `on*`/`javascript:` precedence, the tokenizer, the four rejection reasons).
|
|
20
|
+
* Extend `MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES` in BOTH copies together if a new attribute is ever
|
|
21
|
+
* needed — that is the one piece of drift risk this duplication accepts, the same risk every other
|
|
22
|
+
* hand-copied list in this codebase already accepts.
|
|
23
|
+
*
|
|
24
|
+
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in Tovu's admin
|
|
25
|
+
* but rendered on the public site, so a free-text HTML-attribute passthrough is a stored-XSS vector
|
|
26
|
+
* (`onerror`, `onclick`, `style`, `href="javascript:"`, any `on*` handler) the moment it reaches a
|
|
27
|
+
* public page. A client-side-only check is not a control, since the API accepts whatever a caller
|
|
28
|
+
* sends — this file is what makes the write path (`updateMediaMetadata`) and the render path (a
|
|
29
|
+
* host's own tag-building code) actually enforce it, not just the admin form.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Exact-match attribute names the allowlist accepts beyond the open-ended `data-`/`aria-` prefix
|
|
33
|
+
* families (checked separately in {@link isAllowedMediaHtmlAttributeName}). Picked for the owner's
|
|
34
|
+
* stated near-term uses (animations, custom WebMCP hooks) plus the standard `<img>`/`<video>`
|
|
35
|
+
* attributes those uses actually need; extend this list, not the parser, when a new one is needed —
|
|
36
|
+
* and keep it in sync with the identical constant in Tovu's admin copy (see this file's header).
|
|
37
|
+
*/
|
|
38
|
+
export declare const MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES: readonly ["loading", "decoding", "playsinline", "muted", "loop", "autoplay", "poster"];
|
|
39
|
+
/** Whether `name` is on the allowlist — a well-formed attribute name
|
|
40
|
+
* ({@link MEDIA_HTML_ATTRIBUTE_NAME_PATTERN}) that is either an exact match against
|
|
41
|
+
* {@link MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES} or carries a `data-`/`aria-` prefix (both open-ended
|
|
42
|
+
* families with no fixed suffix list). Case-insensitive: HTML attribute names are themselves
|
|
43
|
+
* case-insensitive, and an operator typing `DATA-FOO` should not slip past a lowercase-only check.
|
|
44
|
+
*
|
|
45
|
+
* @complexity O(n) in the name's length (one anchored regex test), then O(1) — one prefix check,
|
|
46
|
+
* one fixed-length array lookup.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isAllowedMediaHtmlAttributeName(name: string): boolean;
|
|
49
|
+
/** Why one parsed attribute token was rejected — see {@link parseMediaHtmlAttributes}'s own doc for
|
|
50
|
+
* why `event-handler`/`javascript-url` are checked, and reported, ahead of plain allowlist
|
|
51
|
+
* membership. */
|
|
52
|
+
export type MediaHtmlAttributeRejectionReason = "disallowed-name" | "event-handler" | "javascript-url" | "malformed";
|
|
53
|
+
export interface MediaHtmlAttributeError {
|
|
54
|
+
reason: MediaHtmlAttributeRejectionReason;
|
|
55
|
+
/** The exact attribute name (or, for `malformed`, the unparsable fragment) — never a generic
|
|
56
|
+
* "invalid input" (owner requirement: a visible, specific error naming the rejected attribute). */
|
|
57
|
+
attribute: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ParsedMediaHtmlAttributes {
|
|
60
|
+
/** Lowercased attribute name -> value. A boolean attribute (`muted`, written with no
|
|
61
|
+
* `="..."`) maps to `""` — recording only that it was present; how a valueless attribute gets
|
|
62
|
+
* emitted onto the real tag is the renderer's decision, not this parser's. */
|
|
63
|
+
attributes: Record<string, string>;
|
|
64
|
+
/** `null` when every token in the input is allowed and safe; otherwise the FIRST rejection found
|
|
65
|
+
* scanning left to right — one specific, visible reason at a time, not a batch of every problem
|
|
66
|
+
* in the string. */
|
|
67
|
+
error: MediaHtmlAttributeError | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parses the `htmlAttributes` field's free text (`name="value" name2="value2"`, or a bare boolean
|
|
71
|
+
* `name`) into a validated attribute map, rejecting anything not on the allowlist. Nothing here
|
|
72
|
+
* tries to sanitize or escape an otherwise-disallowed name into something safe — it is rejected
|
|
73
|
+
* outright, and the caller must show the reason (not silently drop it) so an operator can tell a
|
|
74
|
+
* typo from a hard "no".
|
|
75
|
+
*
|
|
76
|
+
* @complexity Time O(n) in `text`'s length (one regex pass over it), space O(k) for k parsed
|
|
77
|
+
* attributes.
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseMediaHtmlAttributes(text: string): ParsedMediaHtmlAttributes;
|
|
80
|
+
/** Formats a {@link MediaHtmlAttributeError} into a plain-English message naming the rejected
|
|
81
|
+
* attribute (owner requirement) — the server-side, non-localized counterpart to the admin's own
|
|
82
|
+
* `describeMediaHtmlAttributeError` (which additionally runs the message through `MEDIA_DICT` for
|
|
83
|
+
* the operator's locale). Domain errors elsewhere in this file (`MediaValidationError`'s other
|
|
84
|
+
* messages) are likewise always plain English, so this matches that existing convention rather
|
|
85
|
+
* than inventing a localized-error contract this service has never had.
|
|
86
|
+
*
|
|
87
|
+
* @complexity O(1).
|
|
88
|
+
*/
|
|
89
|
+
export declare function describeMediaHtmlAttributeError(error: MediaHtmlAttributeError): string;
|
|
90
|
+
//# sourceMappingURL=html-attributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-attributes.d.ts","sourceRoot":"","sources":["../../src/media/html-attributes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,wFAQrC,CAAC;AAsBX;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKrE;AAED;;kBAEkB;AAClB,MAAM,MAAM,iCAAiC,GAAG,iBAAiB,GAAG,eAAe,GAAG,gBAAgB,GAAG,WAAW,CAAC;AAErH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,iCAAiC,CAAC;IAC1C;wGACoG;IACpG,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;yBAEqB;IACrB,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACvC;AAuBD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,yBAAyB,CA0BhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAKtF"}
|