@jini-ai/cms 0.3.11 → 0.4.0
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/CHANGELOG.md +59 -0
- package/README.md +47 -19
- package/dist/content-types/agent-tools.d.ts +3 -56
- package/dist/content-types/agent-tools.js +22 -6
- package/dist/content-types/cleanup.d.ts +18 -13
- package/dist/content-types/cleanup.js +16 -11
- package/dist/content-types/errors.d.ts +57 -24
- package/dist/content-types/errors.js +56 -39
- package/dist/content-types/field-defs.d.ts +8 -4
- package/dist/content-types/field-defs.js +19 -14
- package/dist/content-types/index-provisioning.d.ts +61 -47
- package/dist/content-types/index-provisioning.js +90 -66
- package/dist/content-types/index.d.ts +4 -3
- package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
- package/dist/content-types/lifecycle-dispatch.js +5 -4
- package/dist/content-types/lifecycle.d.ts +23 -18
- package/dist/content-types/lifecycle.js +71 -64
- package/dist/content-types/repo.memory.d.ts +5 -4
- package/dist/content-types/repo.memory.js +3 -2
- package/dist/content-types/tool-registrations.d.ts +6 -8
- package/dist/content-types/tool-registrations.js +24 -28
- package/dist/content-types/types.d.ts +17 -13
- package/dist/content-types/types.js +11 -5
- package/dist/content-types/write-service.d.ts +20 -22
- package/dist/content-types/write-service.js +78 -75
- package/dist/core/authorization.d.ts +15 -0
- package/dist/core/authorization.js +2 -0
- package/dist/core/commands/change-set.d.ts +15 -7
- package/dist/core/commands/command.d.ts +45 -26
- package/dist/core/commands/command.js +73 -63
- package/dist/core/entity-liveness.d.ts +11 -7
- package/dist/core/entity-liveness.js +5 -4
- package/dist/core/index.d.ts +12 -5
- package/dist/core/index.js +10 -4
- package/dist/core/ports.d.ts +25 -24
- package/dist/core/ports.js +1 -0
- package/dist/core/tools/args.d.ts +49 -0
- package/dist/core/tools/args.js +46 -0
- package/dist/core/tools/index.d.ts +3 -0
- package/dist/core/tools/index.js +3 -0
- package/dist/entries/agent-tools.d.ts +1 -73
- package/dist/entries/agent-tools.js +19 -13
- package/dist/entries/errors.d.ts +28 -14
- package/dist/entries/errors.js +17 -11
- package/dist/entries/field-validation.d.ts +5 -3
- package/dist/entries/field-validation.js +7 -5
- package/dist/entries/index.d.ts +2 -2
- package/dist/entries/lifecycle-dispatch.d.ts +5 -2
- package/dist/entries/lifecycle-dispatch.js +2 -1
- package/dist/entries/list.d.ts +4 -2
- package/dist/entries/list.js +4 -3
- package/dist/entries/repo.memory.d.ts +12 -10
- package/dist/entries/repo.memory.js +11 -10
- package/dist/entries/tool-registrations.d.ts +6 -8
- package/dist/entries/tool-registrations.js +42 -41
- package/dist/entries/types.d.ts +0 -7
- package/dist/entries/write-service.d.ts +28 -29
- package/dist/entries/write-service.js +114 -105
- package/dist/http/settings/cms-adapter.d.ts +20 -0
- package/dist/http/settings/cms-adapter.js +124 -0
- package/dist/http/settings/contracts.d.ts +135 -0
- package/dist/http/settings/contracts.js +2 -0
- package/dist/http/settings/http.d.ts +10 -0
- package/dist/http/settings/http.js +211 -0
- package/dist/http/settings/index.d.ts +5 -0
- package/dist/http/settings/index.js +4 -0
- package/dist/http/settings/shared.d.ts +68 -0
- package/dist/http/settings/shared.js +54 -0
- package/dist/http/settings/stream.d.ts +24 -0
- package/dist/http/settings/stream.js +147 -0
- package/dist/media/agent-tools.d.ts +1 -66
- package/dist/media/agent-tools.js +5 -1
- package/dist/media/blob-gc-lock.d.ts +6 -2
- package/dist/media/blob-gc-lock.js +4 -2
- package/dist/media/blob-gc.d.ts +64 -51
- package/dist/media/blob-gc.js +62 -52
- package/dist/media/blob-key.d.ts +1 -1
- package/dist/media/content-type-sniffer.d.ts +3 -1
- package/dist/media/content-type-sniffer.js +1 -1
- package/dist/media/html-attributes.d.ts +9 -5
- package/dist/media/html-attributes.js +8 -6
- package/dist/media/image-transformer.js +1 -1
- package/dist/media/image-transformer.sharp.d.ts +6 -0
- package/dist/media/image-transformer.sharp.js +12 -6
- package/dist/media/import/fetch-image.d.ts +24 -0
- package/dist/media/import/fetch-image.js +137 -0
- package/dist/media/import/index.d.ts +3 -0
- package/dist/media/import/index.js +3 -0
- package/dist/media/import/ports.d.ts +38 -0
- package/dist/media/index.d.ts +7 -1
- package/dist/media/index.js +1 -0
- package/dist/media/media-service.d.ts +14 -10
- package/dist/media/media-service.js +57 -52
- package/dist/media/ports.d.ts +3 -2
- package/dist/media/rendition-service.d.ts +3 -3
- package/dist/media/rendition-service.js +38 -37
- package/dist/media/repo.memory.d.ts +18 -7
- package/dist/media/repo.memory.js +12 -6
- package/dist/media/tool-registrations.d.ts +16 -15
- package/dist/media/tool-registrations.js +84 -72
- package/dist/media/transform-lock.d.ts +22 -12
- package/dist/media/transform-lock.js +14 -13
- package/dist/media/transform-registry.d.ts +3 -3
- package/dist/media/transform-registry.js +19 -18
- package/dist/media/transform-types.d.ts +8 -3
- package/dist/media/transform-types.js +10 -5
- package/dist/media/types.d.ts +21 -4
- package/dist/media/types.js +15 -2
- package/dist/navigation/agent-tools.d.ts +1 -80
- package/dist/navigation/agent-tools.js +15 -3
- package/dist/navigation/contracts.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/menu-service.d.ts +57 -40
- package/dist/navigation/menu-service.js +80 -69
- package/dist/navigation/ports.d.ts +1 -1
- package/dist/navigation/read-model.d.ts +3 -1
- package/dist/navigation/read-model.js +2 -2
- package/dist/navigation/reconcile.d.ts +2 -2
- package/dist/navigation/reconcile.js +2 -1
- package/dist/navigation/repo.memory.d.ts +7 -3
- package/dist/navigation/repo.memory.js +4 -2
- package/dist/navigation/resolver.d.ts +4 -1
- package/dist/navigation/resolver.js +1 -1
- package/dist/navigation/tool-registrations.d.ts +6 -8
- package/dist/navigation/tool-registrations.js +44 -42
- package/dist/navigation/types.d.ts +1 -1
- package/dist/presentation/presentation.d.ts +11 -3
- package/dist/presentation/presentation.js +13 -6
- package/dist/presentation/repo.memory.d.ts +6 -2
- package/dist/presentation/repo.memory.js +3 -2
- package/dist/settings/agent-tools.d.ts +2 -75
- package/dist/settings/agent-tools.js +49 -45
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +154 -0
- package/dist/settings/agent-writable-preferences.js +8 -20
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +11 -2
- package/dist/settings/change-feed.js +4 -4
- package/dist/settings/definitions-dispatch.d.ts +8 -3
- package/dist/settings/definitions-dispatch.js +5 -5
- package/dist/settings/dictionaries/index.d.ts +5 -2
- package/dist/settings/dictionaries/index.js +2 -2
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
- package/dist/settings/ensure-definitions.d.ts +5 -5
- package/dist/settings/errors.d.ts +7 -1
- package/dist/settings/errors.js +7 -1
- package/dist/settings/index.d.ts +4 -1
- package/dist/settings/index.js +1 -0
- package/dist/settings/ports.d.ts +6 -5
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +5 -4
- package/dist/settings/purge-service.js +3 -2
- package/dist/settings/settings.d.ts +22 -7
- package/dist/settings/settings.js +47 -26
- package/dist/settings/tool-registrations.d.ts +25 -32
- package/dist/settings/tool-registrations.js +66 -56
- package/dist/settings/types.d.ts +1 -1
- package/dist/settings/ui-tab-definitions.d.ts +1 -1
- package/dist/settings/write-service.d.ts +32 -26
- package/dist/settings/write-service.js +26 -20
- package/dist/taxonomy/agent-tools.d.ts +1 -66
- package/dist/taxonomy/agent-tools.js +25 -15
- package/dist/taxonomy/content-lookup.js +1 -1
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/merge-term.d.ts +7 -4
- package/dist/taxonomy/merge-term.js +7 -5
- package/dist/taxonomy/repo.memory.d.ts +42 -19
- package/dist/taxonomy/repo.memory.js +14 -12
- package/dist/taxonomy/validation-chain.d.ts +39 -18
- package/dist/taxonomy/validation-chain.js +35 -25
- package/dist/taxonomy/write-service.d.ts +103 -44
- package/dist/taxonomy/write-service.js +221 -179
- package/dist/trash/adapters/record-store.d.ts +52 -0
- package/dist/trash/adapters/record-store.js +36 -0
- package/dist/trash/cursor.d.ts +20 -0
- package/dist/trash/cursor.js +36 -0
- package/dist/trash/follow-ups.d.ts +76 -0
- package/dist/trash/follow-ups.js +46 -0
- package/dist/trash/index.d.ts +8 -0
- package/dist/trash/index.js +8 -0
- package/dist/trash/ports.d.ts +308 -0
- package/dist/trash/ports.js +2 -0
- package/dist/trash/repo.memory.d.ts +50 -0
- package/dist/trash/repo.memory.js +114 -0
- package/dist/trash/sweeper.d.ts +77 -0
- package/dist/trash/sweeper.js +127 -0
- package/dist/trash/write-service.d.ts +100 -0
- package/dist/trash/write-service.js +242 -0
- package/dist/workspace/agent-tools.d.ts +1 -59
- package/dist/workspace/agent-tools.js +27 -12
- package/dist/workspace/create.d.ts +31 -6
- package/dist/workspace/create.js +18 -5
- package/dist/workspace/delete.d.ts +18 -13
- package/dist/workspace/delete.js +20 -12
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/repo.memory.d.ts +19 -4
- package/dist/workspace/repo.memory.js +26 -4
- package/dist/workspace/tool-registrations.d.ts +2 -2
- package/dist/workspace/tool-registrations.js +13 -13
- package/dist/workspace/update.d.ts +6 -5
- package/dist/workspace/update.js +9 -8
- package/package.json +44 -38
- package/dist/identity/admin-crud-service.d.ts +0 -280
- package/dist/identity/admin-crud-service.js +0 -468
- package/dist/identity/agent-tool-input.d.ts +0 -58
- package/dist/identity/agent-tool-input.js +0 -126
- package/dist/identity/agent-tools.d.ts +0 -101
- package/dist/identity/agent-tools.js +0 -290
- package/dist/identity/auth-service.d.ts +0 -118
- package/dist/identity/auth-service.js +0 -170
- package/dist/identity/authorize.d.ts +0 -90
- package/dist/identity/authorize.js +0 -94
- package/dist/identity/grant-service.d.ts +0 -198
- package/dist/identity/grant-service.js +0 -361
- package/dist/identity/hasher.d.ts +0 -39
- package/dist/identity/hasher.js +0 -39
- package/dist/identity/index.d.ts +0 -62
- package/dist/identity/index.js +0 -54
- package/dist/identity/password-policy.d.ts +0 -57
- package/dist/identity/password-policy.js +0 -66
- package/dist/identity/permission-migrations.d.ts +0 -86
- package/dist/identity/permission-migrations.js +0 -67
- package/dist/identity/permissions.d.ts +0 -35
- package/dist/identity/permissions.js +0 -603
- package/dist/identity/ports.d.ts +0 -201
- package/dist/identity/repo.memory.d.ts +0 -164
- package/dist/identity/repo.memory.js +0 -224
- package/dist/identity/seed.d.ts +0 -54
- package/dist/identity/seed.js +0 -429
- package/dist/identity/tool-registrations.d.ts +0 -50
- package/dist/identity/tool-registrations.js +0 -419
- package/dist/identity/types.d.ts +0 -185
- package/dist/identity/types.js +0 -64
- package/dist/identity/username.d.ts +0 -12
- package/dist/identity/username.js +0 -14
- /package/dist/{identity → media/import}/ports.js +0 -0
package/dist/identity/seed.js
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import { normalizeUsername } from "./username.js";
|
|
2
|
-
/**
|
|
3
|
-
* @file First-boot identity seed (REQ-09, state.spec `SEED_FIRST_BOOT`).
|
|
4
|
-
*
|
|
5
|
-
* Purpose:
|
|
6
|
-
* Seeds, in one idempotent pass: a `system` principal; a disabled legacy
|
|
7
|
-
* `user-local` principal (REQ-09/EC-09 — so any pre-existing `change_sets`
|
|
8
|
-
* stamped `actorId='user-local'` satisfy the composite FK without rewriting
|
|
9
|
-
* history); four built-in roles (`owner`/`admin`/`editor`/
|
|
10
|
-
* `viewer`) each 1:1 with a built-in policy; and the initial `owner` user.
|
|
11
|
-
*
|
|
12
|
-
* How it relates to the project:
|
|
13
|
-
* - Called once by each composition root (`server/app.ts` in-memory,
|
|
14
|
-
* `server/deps.ts` SQLite-content-with-in-memory-identity) before the
|
|
15
|
-
* server accepts requests. `RouteDeps.identityReady` is this call's
|
|
16
|
-
* promise — auth-adjacent middleware awaits it (see `middleware/dev-auth.ts`).
|
|
17
|
-
*
|
|
18
|
-
* Architectural role:
|
|
19
|
-
* Seed data lives in one place so both composition roots stay identical
|
|
20
|
-
* (mirrors `server/seed.ts`'s reasoning for workspace/post/presentation seed
|
|
21
|
-
* data). Idempotent: re-seeding an already-seeded workspace is a no-op
|
|
22
|
-
* (checked via the owner username).
|
|
23
|
-
*/
|
|
24
|
-
/** REQ-09: the literal id the legacy Article VI actor stamp resolves to. */
|
|
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 = ["*"];
|
|
36
|
-
const BUILTIN_ADMIN_PERMISSIONS = [
|
|
37
|
-
"content.read",
|
|
38
|
-
"content.write",
|
|
39
|
-
"content.publish",
|
|
40
|
-
"content.delete",
|
|
41
|
-
"theme.set",
|
|
42
|
-
// Editing a theme's source files is an admin capability, deliberately NOT in the editor set
|
|
43
|
-
// below (which keeps theme.set): an editor may switch the site between validated themes, but
|
|
44
|
-
// authoring template/CSS source is a build-time-shaped capability whose failure mode is a broken
|
|
45
|
-
// theme, not a different-looking one.
|
|
46
|
-
"theme.edit",
|
|
47
|
-
"plugin.read",
|
|
48
|
-
"plugin.enable",
|
|
49
|
-
"plugin.disable",
|
|
50
|
-
"changeset.read",
|
|
51
|
-
"changeset.revert",
|
|
52
|
-
"member.manage",
|
|
53
|
-
"settings.write",
|
|
54
|
-
"apikey.manage",
|
|
55
|
-
// Migration clause (mirrors the admin.menus.*/admin.integrations.manage
|
|
56
|
-
// precedent above): every freshly-seeded workspace's admin role gets the concrete media.* working
|
|
57
|
-
// actions directly, so it never depends on migrateDeprecatedPermissionGrants(). The umbrella
|
|
58
|
-
// "media.manage" is deliberately not included here (mirrors admin.menus.manage's exclusion
|
|
59
|
-
// above). "media.write" is deliberately dropped from THIS seed list only — the string stays
|
|
60
|
-
// registered (deprecated) in identity/permissions.ts.
|
|
61
|
-
"media.read",
|
|
62
|
-
"media.upload",
|
|
63
|
-
"media.update",
|
|
64
|
-
"media.delete",
|
|
65
|
-
"media.delete.force",
|
|
66
|
-
"media.download_original",
|
|
67
|
-
"media.upload_svg",
|
|
68
|
-
// Menus remediation migration clause: every freshly-seeded
|
|
69
|
-
// workspace gets the 6 new admin.menus.* CRUD strings directly, so it never depends on
|
|
70
|
-
// migrateDeprecatedPermissionGrants() for its own built-in role grants. The now-legacy
|
|
71
|
-
// "navigation.manage" is deliberately dropped from THIS seed list only — the string itself
|
|
72
|
-
// stays registered (deprecated) in identity/permissions.ts. migrateDeprecatedPermissionGrants()
|
|
73
|
-
// now IS wired into live boot (identity/wiring.ts, chained right after seedIdentity resolves)
|
|
74
|
-
// so any pre-existing policy still holding navigation.manage/integration.manage also gains
|
|
75
|
-
// the new string(s) — this fresh-seed list just doesn't need to depend on that fan-out for its
|
|
76
|
-
// own built-in role grants.
|
|
77
|
-
"admin.menus.read",
|
|
78
|
-
"admin.menus.create",
|
|
79
|
-
"admin.menus.update",
|
|
80
|
-
"admin.menus.delete",
|
|
81
|
-
"admin.menus.delete.force",
|
|
82
|
-
"admin.menus.assign",
|
|
83
|
-
// Migration clause (mirrors the admin.menus.* precedent immediately
|
|
84
|
-
// above): every freshly-seeded workspace gets admin.integrations.manage directly, so it never
|
|
85
|
-
// depends on migrateDeprecatedPermissionGrants(). "integration.manage" is deliberately dropped
|
|
86
|
-
// from THIS seed list only — the string stays registered (deprecated) in permissions.ts.
|
|
87
|
-
"admin.integrations.manage",
|
|
88
|
-
// Migration clause: every settings.write holder also
|
|
89
|
-
// gets settings.definitions.manage, so the admin role isn't left
|
|
90
|
-
// fail-closed-locked-out of definition-lifecycle operations it previously
|
|
91
|
-
// reached through the coarse settings.write grant. No separate migration
|
|
92
|
-
// script is needed pre-launch — this seed function is the sole source of
|
|
93
|
-
// built-in role grants (no existing installation's data to migrate yet).
|
|
94
|
-
"settings.definitions.manage",
|
|
95
|
-
"settings.global.write",
|
|
96
|
-
"settings.workspace.write",
|
|
97
|
-
"settings.user.self.write",
|
|
98
|
-
"settings.user.write",
|
|
99
|
-
// Internal audit F2 remediation (2026-07-29): granted directly here so a freshly-seeded
|
|
100
|
-
// workspace never depends on the `settings.user.write` -> `settings.user.read` fan-out in
|
|
101
|
-
// permissions.ts (same "don't depend on migrateDeprecatedPermissionGrants for our own built-in
|
|
102
|
-
// role grants" discipline as the admin.menus.*/admin.integrations.manage clauses above). The
|
|
103
|
-
// fan-out exists for ALREADY-seeded installations, which this list cannot reach because
|
|
104
|
-
// seedIdentity early-returns once an owner user exists.
|
|
105
|
-
"settings.user.read",
|
|
106
|
-
"settings.reset.global",
|
|
107
|
-
"settings.reset.workspace",
|
|
108
|
-
"settings.reset.user",
|
|
109
|
-
"settings.read",
|
|
110
|
-
"settings.read.raw",
|
|
111
|
-
"settings.read.revisions",
|
|
112
|
-
"settings.read.definitions",
|
|
113
|
-
// Every freshly-seeded workspace's built-in admin role
|
|
114
|
-
// gets the one SEO umbrella permission directly, mirroring the admin.menus.* precedent above.
|
|
115
|
-
"admin.seo.manage",
|
|
116
|
-
// The AI Assistant section's umbrella permission, granted directly here for the same reason
|
|
117
|
-
// `admin.seo.manage` above is: a freshly-seeded workspace's built-in admin must be able to reach
|
|
118
|
-
// the public assistant's master switch without depending on any deprecated-grant fan-out. Turning
|
|
119
|
-
// the public assistant ON is still a deliberate act — the SETTING defaults to off
|
|
120
|
-
// (`assistant/public-assistant-settings.ts`); this only grants the ability to flip it.
|
|
121
|
-
"admin.assistant.manage",
|
|
122
|
-
// Workspace Administration: admin gets workspace.manage
|
|
123
|
-
// directly at seed — a distinct grant from settings.write, not owner-only (unlike user.manage/
|
|
124
|
-
// role.manage below).
|
|
125
|
-
"workspace.manage",
|
|
126
|
-
// Owner-only per REQ-09: "user.manage", "role.manage" are deliberately absent.
|
|
127
|
-
];
|
|
128
|
-
const BUILTIN_EDITOR_PERMISSIONS = [
|
|
129
|
-
"content.read",
|
|
130
|
-
"content.write",
|
|
131
|
-
"content.publish",
|
|
132
|
-
"content.delete",
|
|
133
|
-
// Migration clause: editor gets the ordinary media working actions
|
|
134
|
-
// (read/upload/update/trash) but, unlike admin above, NOT media.delete.force (destructive
|
|
135
|
-
// hard-purge), media.download_original (mint-only access to sensitive originals), or
|
|
136
|
-
// media.upload_svg (XSS-risk-gated capability) — mirrors the Forms admin.forms.manage vs
|
|
137
|
-
// admin.forms.submissions.* PII-split precedent rather than granting editor
|
|
138
|
-
// everything admin.write's single flat string previously implied. "media.write" is deliberately
|
|
139
|
-
// dropped from THIS seed list only — the string stays registered (deprecated) in
|
|
140
|
-
// identity/permissions.ts. Still a subset of admin's media.* set above (AC-12's owner ⊇ admin ⊇
|
|
141
|
-
// editor subset property).
|
|
142
|
-
"media.read",
|
|
143
|
-
"media.upload",
|
|
144
|
-
"media.update",
|
|
145
|
-
"media.delete",
|
|
146
|
-
"theme.set",
|
|
147
|
-
];
|
|
148
|
-
const BUILTIN_VIEWER_PERMISSIONS = ["content.read", "changeset.read", "plugin.read"];
|
|
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 (one host
|
|
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));
|
|
217
|
-
for (const permission of permissions) {
|
|
218
|
-
if (held.has(permission))
|
|
219
|
-
continue;
|
|
220
|
-
await deps.repos.policyPermissions.save({
|
|
221
|
-
id: deps.idGen.newId(),
|
|
222
|
-
workspaceId,
|
|
223
|
-
policyId,
|
|
224
|
-
permission,
|
|
225
|
-
resourceType: null,
|
|
226
|
-
constraintJson: null,
|
|
227
|
-
});
|
|
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 });
|
|
241
|
-
void nowIso; // reserved for a future createdAt column on roles/policies
|
|
242
|
-
return { roleId, policyId };
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
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: the host'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
|
-
* the host'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.
|
|
307
|
-
*
|
|
308
|
-
* @complexity O(1) — fixed small number of inserts (4 roles/policies, ~2
|
|
309
|
-
* dozen policy_permissions, 3 principals, 1 user, 1 role assignment).
|
|
310
|
-
* @overallScore 100
|
|
311
|
-
*/
|
|
312
|
-
export async function seedIdentity(required) {
|
|
313
|
-
const { deps, input } = required;
|
|
314
|
-
const workspaceId = input.workspaceId;
|
|
315
|
-
const ownerUsername = normalizeUsername(input.ownerUsername ?? "admin");
|
|
316
|
-
const ownerPassword = input.ownerPassword;
|
|
317
|
-
const nowIso = deps.clock.nowIso();
|
|
318
|
-
const existingOwnerUser = await deps.repos.users.findByUsername({
|
|
319
|
-
workspaceId,
|
|
320
|
-
username: ownerUsername,
|
|
321
|
-
});
|
|
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
|
-
});
|
|
331
|
-
const existingSystem = (await deps.repos.principals.list({ workspaceId })).find((row) => row.kind === "system" && row.id !== LEGACY_USER_LOCAL_PRINCIPAL_ID);
|
|
332
|
-
return {
|
|
333
|
-
ownerPrincipalId: existingOwnerUser.principalId,
|
|
334
|
-
systemPrincipalId: existingSystem?.id ?? LEGACY_USER_LOCAL_PRINCIPAL_ID,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
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
|
-
}
|
|
352
|
-
// REQ-09/EC-09: disabled legacy actor so historical `actorId='user-local'`
|
|
353
|
-
// change-sets resolve without rewriting history.
|
|
354
|
-
await deps.repos.principals.save({
|
|
355
|
-
id: LEGACY_USER_LOCAL_PRINCIPAL_ID,
|
|
356
|
-
workspaceId,
|
|
357
|
-
kind: "system",
|
|
358
|
-
displayName: "Legacy actor (pre-identity)",
|
|
359
|
-
status: "disabled",
|
|
360
|
-
disabledAt: nowIso,
|
|
361
|
-
createdAt: nowIso,
|
|
362
|
-
});
|
|
363
|
-
const { roleId: ownerRoleId } = await seedBuiltinRoleWithPolicy({
|
|
364
|
-
deps,
|
|
365
|
-
workspaceId,
|
|
366
|
-
name: OWNER_ROLE_NAME,
|
|
367
|
-
permissions: OWNER_ROLE_PERMISSIONS,
|
|
368
|
-
nowIso,
|
|
369
|
-
});
|
|
370
|
-
await seedBuiltinRoleWithPolicy({
|
|
371
|
-
deps,
|
|
372
|
-
workspaceId,
|
|
373
|
-
name: "admin",
|
|
374
|
-
permissions: BUILTIN_ADMIN_PERMISSIONS,
|
|
375
|
-
nowIso,
|
|
376
|
-
});
|
|
377
|
-
await seedBuiltinRoleWithPolicy({
|
|
378
|
-
deps,
|
|
379
|
-
workspaceId,
|
|
380
|
-
name: "editor",
|
|
381
|
-
permissions: BUILTIN_EDITOR_PERMISSIONS,
|
|
382
|
-
nowIso,
|
|
383
|
-
});
|
|
384
|
-
await seedBuiltinRoleWithPolicy({
|
|
385
|
-
deps,
|
|
386
|
-
workspaceId,
|
|
387
|
-
name: "viewer",
|
|
388
|
-
permissions: BUILTIN_VIEWER_PERMISSIONS,
|
|
389
|
-
nowIso,
|
|
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);
|
|
397
|
-
const ownerPrincipalId = deps.idGen.newId();
|
|
398
|
-
await deps.repos.principals.save({
|
|
399
|
-
id: ownerPrincipalId,
|
|
400
|
-
workspaceId,
|
|
401
|
-
kind: "user",
|
|
402
|
-
displayName: "Owner",
|
|
403
|
-
status: "active",
|
|
404
|
-
createdAt: nowIso,
|
|
405
|
-
});
|
|
406
|
-
await deps.repos.users.save({
|
|
407
|
-
principalId: ownerPrincipalId,
|
|
408
|
-
workspaceId,
|
|
409
|
-
username: ownerUsername,
|
|
410
|
-
email: input.ownerEmail,
|
|
411
|
-
passwordHash: ownerPasswordHash,
|
|
412
|
-
});
|
|
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({
|
|
416
|
-
workspaceId,
|
|
417
|
-
principalId: ownerPrincipalId,
|
|
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
|
-
}
|
|
427
|
-
return { ownerPrincipalId, systemPrincipalId };
|
|
428
|
-
}
|
|
429
|
-
//# sourceMappingURL=seed.js.map
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Identity's half of the agent-tool authorization wiring: maps `agent-tools.ts`'s fifteen catalog
|
|
3
|
-
* entries onto the users/roles/policies transitions `grant-service.ts`/`admin-crud-service.ts`
|
|
4
|
-
* export, plus the three repo-backed reads, as `ToolRegistration`s.
|
|
5
|
-
*
|
|
6
|
-
* `ToolPolicy.authorize` is a pass-through for all fifteen (see `buildDomainRegistrations`) for the
|
|
7
|
-
* "one evaluator" reason: the gate is the domain layer's, reached identically by every
|
|
8
|
-
* tool kind — mutations through their own service function, which opens with the check, and reads
|
|
9
|
-
* through {@link assertIdentityReadAllowed}, which calls the domain's own exported
|
|
10
|
-
* `assertCallerHasAnyPermission` rather than re-deriving an OR gate. A second check here would be a
|
|
11
|
-
* duplicate evaluator, and one any non-tool caller of the same service function would bypass. See
|
|
12
|
-
* `assistant/__tests__/tool-registrations.identity-authorization.test.ts`.
|
|
13
|
-
*/
|
|
14
|
-
import { type DerivedRiskByToolId, type ToolRegistration } from "../core/tools/registration-kit.js";
|
|
15
|
-
import type { PasswordHasherPort, PolicyPermissionRepoPort, PolicyRepoPort, PrincipalPolicyRepoPort, PrincipalRepoPort, PrincipalRoleRepoPort, RolePolicyRepoPort, RoleRepoPort, SessionRepoPort, UserRepoPort } from "./ports.js";
|
|
16
|
-
/**
|
|
17
|
-
* The exact slice of the route-deps bag Identity's tool handlers read. Declared structurally
|
|
18
|
-
* (rather than importing `server/routes/types`'s `RouteDeps`) so this module carries no back-edge
|
|
19
|
-
* into the composition root. `server/routes/*` satisfies this structurally by passing its existing
|
|
20
|
-
* `RouteDeps` (in practice `UsersRouteDeps`, a `Pick<RouteDeps, ...>` subset — a strict superset of
|
|
21
|
-
* this shape) object; nothing there changes.
|
|
22
|
-
*/
|
|
23
|
-
export interface IdentityToolDeps {
|
|
24
|
-
workspaceId: string;
|
|
25
|
-
clock: {
|
|
26
|
-
nowIso(): string;
|
|
27
|
-
};
|
|
28
|
-
idGen: {
|
|
29
|
-
newId(): string;
|
|
30
|
-
};
|
|
31
|
-
principalRepo: PrincipalRepoPort;
|
|
32
|
-
userRepo: UserRepoPort;
|
|
33
|
-
sessionRepo: SessionRepoPort;
|
|
34
|
-
roleRepo: RoleRepoPort;
|
|
35
|
-
policyRepo: PolicyRepoPort;
|
|
36
|
-
policyPermissionRepo: PolicyPermissionRepoPort;
|
|
37
|
-
rolePolicyRepo: RolePolicyRepoPort;
|
|
38
|
-
principalRoleRepo: PrincipalRoleRepoPort;
|
|
39
|
-
principalPolicyRepo: PrincipalPolicyRepoPort;
|
|
40
|
-
passwordHasher: PasswordHasherPort;
|
|
41
|
-
ownerPrincipalId: Promise<string>;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
45
|
-
* calls. See `DerivedRiskByToolId` in the kit for why it is independent of the catalog's own
|
|
46
|
-
* `sideEffects` declaration.
|
|
47
|
-
*/
|
|
48
|
-
export declare const identityDerivedRisk: DerivedRiskByToolId;
|
|
49
|
-
export declare function buildIdentityRegistrations(routeDeps: IdentityToolDeps): ToolRegistration[];
|
|
50
|
-
//# sourceMappingURL=tool-registrations.d.ts.map
|