@chatlychat/cli 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/commands/auth.d.ts +1 -1
  2. package/dist/commands/auth.d.ts.map +1 -1
  3. package/dist/commands/auth.js +40 -3
  4. package/dist/commands/auth.js.map +1 -1
  5. package/dist/commands/init.d.ts +29 -0
  6. package/dist/commands/init.d.ts.map +1 -0
  7. package/dist/commands/init.js +176 -0
  8. package/dist/commands/init.js.map +1 -0
  9. package/dist/commands/integrations.d.ts +27 -0
  10. package/dist/commands/integrations.d.ts.map +1 -1
  11. package/dist/commands/integrations.js +133 -3
  12. package/dist/commands/integrations.js.map +1 -1
  13. package/dist/commands/platform-as-code.d.ts +54 -0
  14. package/dist/commands/platform-as-code.d.ts.map +1 -0
  15. package/dist/commands/platform-as-code.js +185 -0
  16. package/dist/commands/platform-as-code.js.map +1 -0
  17. package/dist/commands/workspace-as-code.d.ts +65 -11
  18. package/dist/commands/workspace-as-code.d.ts.map +1 -1
  19. package/dist/commands/workspace-as-code.js +35 -177
  20. package/dist/commands/workspace-as-code.js.map +1 -1
  21. package/dist/help.d.ts +1 -1
  22. package/dist/help.d.ts.map +1 -1
  23. package/dist/help.js +30 -1
  24. package/dist/help.js.map +1 -1
  25. package/dist/init/shared.d.ts +61 -0
  26. package/dist/init/shared.d.ts.map +1 -0
  27. package/dist/init/shared.js +110 -0
  28. package/dist/init/shared.js.map +1 -0
  29. package/dist/init/templates.d.ts +29 -0
  30. package/dist/init/templates.d.ts.map +1 -0
  31. package/dist/init/templates.js +362 -0
  32. package/dist/init/templates.js.map +1 -0
  33. package/dist/pac/platform-document.d.ts +65 -0
  34. package/dist/pac/platform-document.d.ts.map +1 -0
  35. package/dist/pac/platform-document.js +149 -0
  36. package/dist/pac/platform-document.js.map +1 -0
  37. package/dist/pac/platform-live.d.ts +65 -0
  38. package/dist/pac/platform-live.d.ts.map +1 -0
  39. package/dist/pac/platform-live.js +263 -0
  40. package/dist/pac/platform-live.js.map +1 -0
  41. package/dist/pac/platform-resources.d.ts +141 -0
  42. package/dist/pac/platform-resources.d.ts.map +1 -0
  43. package/dist/pac/platform-resources.js +509 -0
  44. package/dist/pac/platform-resources.js.map +1 -0
  45. package/dist/run.d.ts.map +1 -1
  46. package/dist/run.js +23 -2
  47. package/dist/run.js.map +1 -1
  48. package/dist/wac/apply.d.ts.map +1 -1
  49. package/dist/wac/apply.js +52 -15
  50. package/dist/wac/apply.js.map +1 -1
  51. package/dist/wac/diff-warnings.d.ts +35 -0
  52. package/dist/wac/diff-warnings.d.ts.map +1 -0
  53. package/dist/wac/diff-warnings.js +87 -0
  54. package/dist/wac/diff-warnings.js.map +1 -0
  55. package/dist/wac/diff.d.ts +40 -70
  56. package/dist/wac/diff.d.ts.map +1 -1
  57. package/dist/wac/diff.js +90 -91
  58. package/dist/wac/diff.js.map +1 -1
  59. package/dist/wac/document-key.d.ts +30 -0
  60. package/dist/wac/document-key.d.ts.map +1 -0
  61. package/dist/wac/document-key.js +80 -0
  62. package/dist/wac/document-key.js.map +1 -0
  63. package/dist/wac/document.d.ts +75 -65
  64. package/dist/wac/document.d.ts.map +1 -1
  65. package/dist/wac/document.js +60 -46
  66. package/dist/wac/document.js.map +1 -1
  67. package/dist/wac/execute.d.ts +25 -0
  68. package/dist/wac/execute.d.ts.map +1 -0
  69. package/dist/wac/execute.js +126 -0
  70. package/dist/wac/execute.js.map +1 -0
  71. package/dist/wac/load.d.ts +15 -0
  72. package/dist/wac/load.d.ts.map +1 -0
  73. package/dist/wac/load.js +91 -0
  74. package/dist/wac/load.js.map +1 -0
  75. package/dist/wac/resource-def.d.ts +85 -1
  76. package/dist/wac/resource-def.d.ts.map +1 -1
  77. package/dist/wac/resource-def.js +4 -0
  78. package/dist/wac/resource-def.js.map +1 -1
  79. package/dist/wac/values.d.ts +7 -0
  80. package/dist/wac/values.d.ts.map +1 -1
  81. package/dist/wac/values.js +16 -0
  82. package/dist/wac/values.js.map +1 -1
  83. package/package.json +2 -2
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The resource table `chatly platform plan` and `chatly platform apply` work
3
+ * from — the Platform API as a file (CHT-F04).
4
+ *
5
+ * ═══ WHY THIS IS A SECOND TABLE AND NOT MORE ROWS IN `wac/resources.ts` ═══
6
+ *
7
+ * A different CREDENTIAL and a different SHAPE, and either one on its own
8
+ * would be enough.
9
+ *
10
+ * The credential: every route here is behind `PlatformGuard`
11
+ * (`apps/api/src/modules/platform/platform.guard.ts`), which resolves a `cp_`
12
+ * platform credential and refuses anything else. A workspace `ck_` key
13
+ * presented to `/v1/platform/*` is not merely unauthorised — that guard's own
14
+ * header spells out that there is no code path which turns a platform
15
+ * credential into a membership, and none which turns a key into a platform
16
+ * principal. So one document cannot describe both surfaces: it would be one
17
+ * file needing two credentials, and the first thing it would do is fail half
18
+ * its reads.
19
+ *
20
+ * The shape: a workspace-level resource lives at a flat collection path and
21
+ * names its parent in the BODY. Every resource here lives under
22
+ * `/v1/platform/workspaces/{workspaceId}/…` and names its parent in the PATH,
23
+ * and a platform document describes SEVERAL workspaces at once — which is the
24
+ * whole point of it, and the reason `naturalKey` below is a synthetic
25
+ * `address` rather than an email or a name. `a@b.c` identifies a member
26
+ * inside one tenant and nothing at all across a document that provisions
27
+ * four.
28
+ *
29
+ * ═══ VERIFIED AGAINST HANDLERS ════════════════════════════════════════════
30
+ *
31
+ * Every path, key, flag and field-set below was read off
32
+ * `apps/api/src/modules/platform/**` on 2026-10-25. CLAUDE.md records why
33
+ * that is not optional — the SDK manifest's first version listed two routes
34
+ * that do not exist and had its `paged` flag wrong in both directions — and
35
+ * `packages/sdk-spec/surface.json` holds no `/v1/platform` path at all, so
36
+ * there was no manifest to read even if one were trusted. The citations:
37
+ *
38
+ * workspaces platform.controller.ts:199 (POST),
39
+ * platform-workspace-read.controller.ts:115 (GET, PAGED)
40
+ * users platform.controller.ts:229 (POST), :249 (DELETE);
41
+ * read from the DETAIL route's `users[]`,
42
+ * platform-workspace-read.controller.ts:160
43
+ * apiKeys platform.controller.ts:422 (POST),
44
+ * platform-api-keys.controller.ts:129 (GET), :159 (DELETE)
45
+ * botCredentials platform.controller.ts:483 (POST), :465 (GET), :524 (DELETE)
46
+ * knowledgeSources platform-knowledge.controller.ts:117 (POST), :161 (GET),
47
+ * :197 (DELETE)
48
+ *
49
+ * ═══ FIVE THINGS THE HANDLERS DECIDED, EACH OF WHICH WOULD BE A LIVE BUG ══
50
+ *
51
+ * 1. **NOTHING on this surface has an update route.** There is no PATCH on a
52
+ * platform workspace, a membership, a key, a bot credential or a
53
+ * knowledge source — grep the five controllers and the only `@Patch` in
54
+ * the module is on `platform-admin` (the operator console) and
55
+ * `platform-bot` (a message's blocks). So every `update` below is `[]`
56
+ * with an `updateBlocked` sentence, and a document that changes a field
57
+ * on an existing resource is reported as un-appliable rather than sent
58
+ * somewhere that would strip it and answer 200. This is the single most
59
+ * important fact about planning this surface and it is why the table
60
+ * reads as thin as it does.
61
+ *
62
+ * 2. **A revoked credential stays in the list.** `GET …/api_keys` says so in
63
+ * its own route description — "a revoked key stays listed, with
64
+ * `revokedAt` set" — and `listBots` orders BY `revokedAt` precisely
65
+ * because revoked rows are in the answer. Matching a document row to one
66
+ * would report `noop` for a credential that authenticates nothing: the
67
+ * plan would say the tenant matches the file while the pipeline the file
68
+ * describes is locked out. Hence `ignoreLive` on both.
69
+ *
70
+ * 3. **A knowledge source is written as `url` and read back as `seedUrl`.**
71
+ * `knowledgeSourceSchema` takes `url`; `KNOWLEDGE_SOURCE_FIELDS` returns
72
+ * `seedUrl`. Without the projection below, `url` compares against a field
73
+ * that does not exist, reports a change on every run, and — since there
74
+ * is no update route — is reported as permanently un-appliable. Exactly
75
+ * the `externalAgent` shape in the workspace table.
76
+ *
77
+ * 4. **`POST /v1/platform/workspaces` 409s on a taken slug.** It is a strict
78
+ * create and not an upsert (`createWorkspace` reads the slug and throws
79
+ * `SlugTakenError` before it writes anything), and `workspaces.slug` is
80
+ * unique ACROSS the installation rather than per reseller. So a slug is a
81
+ * real natural key, and `plan` can tell a create from a noop without a
82
+ * uuid — which is the property that makes a portable document possible.
83
+ *
84
+ * 5. **A soft-deleted workspace is not in the default list** and a SUSPENDED
85
+ * one is (`platform-workspace-read.controller.ts`: "omitting `?status=`
86
+ * still answers every workspace that is not soft-deleted — suspended ones
87
+ * included"). A suspended tenant therefore matches its document row and
88
+ * reports `noop`, which is correct: it exists, and there is no platform
89
+ * route that un-suspends it. `status` is `unwritable` so nobody can try.
90
+ *
91
+ * ═══ WHAT IS DELIBERATELY NOT HERE ════════════════════════════════════════
92
+ *
93
+ * `REFUSED_KEYS` below, by name, each with the reason. Login links are a
94
+ * mint, not a declaration. Channel config, IDV mode, business hours and
95
+ * routing WERE refused here because no platform route read them back; since
96
+ * `GET`/`POST /v1/platform/workspaces/{id}/settings` they are the `settings`
97
+ * row, and their old names point there.
98
+ */
99
+ import { type ResourceDef } from '../wac/resource-def.js';
100
+ export { addressOf } from '../wac/resource-def.js';
101
+ /**
102
+ * The synthetic natural key, on both sides. Never authored, never diffed.
103
+ *
104
+ * `document.ts`'s rule is that a field the server returns and no write path
105
+ * accepts must be refused by name, because zod strips it and answers 200. The
106
+ * address is not that — it exists on neither side of the wire — but a
107
+ * document that sets it is still a document whose author has misunderstood
108
+ * something, and the refusal is free. So it is in every `unwritable` list.
109
+ */
110
+ export declare const ADDRESS = "address";
111
+ /**
112
+ * Dependency order, and it is the array order.
113
+ *
114
+ * `workspaces` first and everything else after, because every other resource
115
+ * here names a workspace in its request PATH and a single `apply` may be
116
+ * about to create that workspace. `diff.ts` resolves the reference where it
117
+ * can and notes "this plan creates it first" where it cannot; `apply.ts`
118
+ * fills the path from the id the create actually returned, and skips the
119
+ * child outright if the parent failed.
120
+ */
121
+ export declare const PLATFORM_RESOURCES: ReadonlyArray<ResourceDef>;
122
+ export declare const PLATFORM_RESOURCE_TYPES: ReadonlyArray<string>;
123
+ export declare function platformResourceDef(type: string): ResourceDef | undefined;
124
+ /**
125
+ * Keys a platform document may NOT declare, refused by name with the reason
126
+ * rather than reported as "unknown resource type", which would send the
127
+ * reader hunting for a spelling mistake (`wac/document.ts` does the same for
128
+ * `channels:`).
129
+ *
130
+ * Three are CHT-F04's old top-level names, kept as pointers to the `settings`
131
+ * row that now plans them. The fourth is real: **a login link is a mint, not
132
+ * a declaration.** Its plaintext exists in one response and nowhere else,
133
+ * `GET /v1/platform/login_links` returns no token and no hash, and a document
134
+ * that "declared" one would mint a new single-use credential on every apply.
135
+ */
136
+ export declare const REFUSED_KEYS: Readonly<Record<string, string>>;
137
+ /** Every key `resources:` accepts, including the ones it refuses by name. */
138
+ export declare const PLATFORM_RESOURCE_KEYS: ReadonlyArray<string>;
139
+ /** The address of one child row, for a message or a key. */
140
+ export declare function childAddress(parentKey: string, childKey: string): string;
141
+ //# sourceMappingURL=platform-resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-resources.d.ts","sourceRoot":"","sources":["../../src/pac/platform-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiGG;AAEH,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAKnD;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,YAAY,CAAC;AAQjC;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,WAAW,CA6VzD,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,MAAM,CAAyC,CAAC;AAIpG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEzE;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBzD,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,MAAM,CAGxD,CAAC;AAEF,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExE"}
@@ -0,0 +1,509 @@
1
+ /**
2
+ * The resource table `chatly platform plan` and `chatly platform apply` work
3
+ * from — the Platform API as a file (CHT-F04).
4
+ *
5
+ * ═══ WHY THIS IS A SECOND TABLE AND NOT MORE ROWS IN `wac/resources.ts` ═══
6
+ *
7
+ * A different CREDENTIAL and a different SHAPE, and either one on its own
8
+ * would be enough.
9
+ *
10
+ * The credential: every route here is behind `PlatformGuard`
11
+ * (`apps/api/src/modules/platform/platform.guard.ts`), which resolves a `cp_`
12
+ * platform credential and refuses anything else. A workspace `ck_` key
13
+ * presented to `/v1/platform/*` is not merely unauthorised — that guard's own
14
+ * header spells out that there is no code path which turns a platform
15
+ * credential into a membership, and none which turns a key into a platform
16
+ * principal. So one document cannot describe both surfaces: it would be one
17
+ * file needing two credentials, and the first thing it would do is fail half
18
+ * its reads.
19
+ *
20
+ * The shape: a workspace-level resource lives at a flat collection path and
21
+ * names its parent in the BODY. Every resource here lives under
22
+ * `/v1/platform/workspaces/{workspaceId}/…` and names its parent in the PATH,
23
+ * and a platform document describes SEVERAL workspaces at once — which is the
24
+ * whole point of it, and the reason `naturalKey` below is a synthetic
25
+ * `address` rather than an email or a name. `a@b.c` identifies a member
26
+ * inside one tenant and nothing at all across a document that provisions
27
+ * four.
28
+ *
29
+ * ═══ VERIFIED AGAINST HANDLERS ════════════════════════════════════════════
30
+ *
31
+ * Every path, key, flag and field-set below was read off
32
+ * `apps/api/src/modules/platform/**` on 2026-10-25. CLAUDE.md records why
33
+ * that is not optional — the SDK manifest's first version listed two routes
34
+ * that do not exist and had its `paged` flag wrong in both directions — and
35
+ * `packages/sdk-spec/surface.json` holds no `/v1/platform` path at all, so
36
+ * there was no manifest to read even if one were trusted. The citations:
37
+ *
38
+ * workspaces platform.controller.ts:199 (POST),
39
+ * platform-workspace-read.controller.ts:115 (GET, PAGED)
40
+ * users platform.controller.ts:229 (POST), :249 (DELETE);
41
+ * read from the DETAIL route's `users[]`,
42
+ * platform-workspace-read.controller.ts:160
43
+ * apiKeys platform.controller.ts:422 (POST),
44
+ * platform-api-keys.controller.ts:129 (GET), :159 (DELETE)
45
+ * botCredentials platform.controller.ts:483 (POST), :465 (GET), :524 (DELETE)
46
+ * knowledgeSources platform-knowledge.controller.ts:117 (POST), :161 (GET),
47
+ * :197 (DELETE)
48
+ *
49
+ * ═══ FIVE THINGS THE HANDLERS DECIDED, EACH OF WHICH WOULD BE A LIVE BUG ══
50
+ *
51
+ * 1. **NOTHING on this surface has an update route.** There is no PATCH on a
52
+ * platform workspace, a membership, a key, a bot credential or a
53
+ * knowledge source — grep the five controllers and the only `@Patch` in
54
+ * the module is on `platform-admin` (the operator console) and
55
+ * `platform-bot` (a message's blocks). So every `update` below is `[]`
56
+ * with an `updateBlocked` sentence, and a document that changes a field
57
+ * on an existing resource is reported as un-appliable rather than sent
58
+ * somewhere that would strip it and answer 200. This is the single most
59
+ * important fact about planning this surface and it is why the table
60
+ * reads as thin as it does.
61
+ *
62
+ * 2. **A revoked credential stays in the list.** `GET …/api_keys` says so in
63
+ * its own route description — "a revoked key stays listed, with
64
+ * `revokedAt` set" — and `listBots` orders BY `revokedAt` precisely
65
+ * because revoked rows are in the answer. Matching a document row to one
66
+ * would report `noop` for a credential that authenticates nothing: the
67
+ * plan would say the tenant matches the file while the pipeline the file
68
+ * describes is locked out. Hence `ignoreLive` on both.
69
+ *
70
+ * 3. **A knowledge source is written as `url` and read back as `seedUrl`.**
71
+ * `knowledgeSourceSchema` takes `url`; `KNOWLEDGE_SOURCE_FIELDS` returns
72
+ * `seedUrl`. Without the projection below, `url` compares against a field
73
+ * that does not exist, reports a change on every run, and — since there
74
+ * is no update route — is reported as permanently un-appliable. Exactly
75
+ * the `externalAgent` shape in the workspace table.
76
+ *
77
+ * 4. **`POST /v1/platform/workspaces` 409s on a taken slug.** It is a strict
78
+ * create and not an upsert (`createWorkspace` reads the slug and throws
79
+ * `SlugTakenError` before it writes anything), and `workspaces.slug` is
80
+ * unique ACROSS the installation rather than per reseller. So a slug is a
81
+ * real natural key, and `plan` can tell a create from a noop without a
82
+ * uuid — which is the property that makes a portable document possible.
83
+ *
84
+ * 5. **A soft-deleted workspace is not in the default list** and a SUSPENDED
85
+ * one is (`platform-workspace-read.controller.ts`: "omitting `?status=`
86
+ * still answers every workspace that is not soft-deleted — suspended ones
87
+ * included"). A suspended tenant therefore matches its document row and
88
+ * reports `noop`, which is correct: it exists, and there is no platform
89
+ * route that un-suspends it. `status` is `unwritable` so nobody can try.
90
+ *
91
+ * ═══ WHAT IS DELIBERATELY NOT HERE ════════════════════════════════════════
92
+ *
93
+ * `REFUSED_KEYS` below, by name, each with the reason. Login links are a
94
+ * mint, not a declaration. Channel config, IDV mode, business hours and
95
+ * routing WERE refused here because no platform route read them back; since
96
+ * `GET`/`POST /v1/platform/workspaces/{id}/settings` they are the `settings`
97
+ * row, and their old names point there.
98
+ */
99
+ import { addressOf } from '../wac/resource-def.js';
100
+ export { addressOf } from '../wac/resource-def.js';
101
+ /** Fields every platform row carries that no write path accepts. */
102
+ const SERVER_OWNED = ['id', 'workspaceId', 'createdAt', 'updatedAt'];
103
+ /**
104
+ * The synthetic natural key, on both sides. Never authored, never diffed.
105
+ *
106
+ * `document.ts`'s rule is that a field the server returns and no write path
107
+ * accepts must be refused by name, because zod strips it and answers 200. The
108
+ * address is not that — it exists on neither side of the wire — but a
109
+ * document that sets it is still a document whose author has misunderstood
110
+ * something, and the refusal is free. So it is in every `unwritable` list.
111
+ */
112
+ export const ADDRESS = 'address';
113
+ /** The head of a credential-list row that says it is no longer a credential. */
114
+ function revoked(row) {
115
+ const at = row['revokedAt'];
116
+ return typeof at === 'string' && at.length > 0;
117
+ }
118
+ /**
119
+ * Dependency order, and it is the array order.
120
+ *
121
+ * `workspaces` first and everything else after, because every other resource
122
+ * here names a workspace in its request PATH and a single `apply` may be
123
+ * about to create that workspace. `diff.ts` resolves the reference where it
124
+ * can and notes "this plan creates it first" where it cannot; `apply.ts`
125
+ * fills the path from the id the create actually returned, and skips the
126
+ * child outright if the parent failed.
127
+ */
128
+ export const PLATFORM_RESOURCES = [
129
+ {
130
+ type: 'workspaces',
131
+ label: 'workspace',
132
+ naturalKey: 'slug',
133
+ keySource: 'authored',
134
+ /*
135
+ * TRUE, and more strongly than anywhere in the workspace table: the
136
+ * unique index on `workspaces.slug` is installation-wide, so a slug this
137
+ * credential cannot see may still be taken. That is a 409 on create
138
+ * rather than a mismatch, and the 409's message names the slug.
139
+ */
140
+ keyEnforced: true,
141
+ list: { path: '/v1/platform/workspaces', paged: true },
142
+ idField: 'id',
143
+ create: { path: '/v1/platform/workspaces' },
144
+ update: [],
145
+ updateBlocked: 'there is no PATCH on a platform workspace — not for `name`, not for `plan`, not for ' +
146
+ '`sandbox`. `POST /v1/platform/workspaces` creates and 409s on a slug that is taken, and ' +
147
+ 'the only other writes are delete and restore. Rename a tenant from inside it ' +
148
+ '(`PATCH /v1/workspace/settings` with a `ck_` key), which is a different credential and ' +
149
+ 'therefore a different document.',
150
+ /*
151
+ * `null`, and this is the most consequential safety decision in the file.
152
+ *
153
+ * `DELETE /v1/platform/workspaces/{id}` EXISTS and works — it soft-deletes
154
+ * the tenant and starts a 30-day purge clock. `--prune` will not call it.
155
+ *
156
+ * Everything else `--prune` can delete on this surface is recoverable by
157
+ * re-running `apply`: a membership is re-added, a key or a bot credential
158
+ * is re-minted (with a new token, which the plan says out loud), a
159
+ * knowledge source is re-crawled and its articles were never deleted. A
160
+ * workspace is the one row where prune would destroy a customer's
161
+ * conversations, contacts and tickets because a slug was spelled
162
+ * differently in a file — and a platform document is a SUBSET of the
163
+ * tenants a credential owns, so "not in the document" is the normal state
164
+ * of a healthy workspace rather than evidence of anything.
165
+ *
166
+ * The reader never offers the chance anyway (see `platform-live.ts`: it
167
+ * reads only the slugs the document declares, so an undeclared tenant is
168
+ * not even an orphan). This is the second lock, for the case where
169
+ * somebody changes that.
170
+ */
171
+ remove: null,
172
+ removeBlocked: '`--prune` will not delete a tenant. `DELETE /v1/platform/workspaces/{id}` exists and ' +
173
+ 'soft-deletes the workspace with a 30-day purge clock — every conversation, contact and ' +
174
+ 'ticket in it — and a platform document describes a SUBSET of the tenants a credential ' +
175
+ 'owns, so a workspace this file does not mention is the normal state of a healthy ' +
176
+ 'customer. Delete one deliberately, by id, with that route.',
177
+ opaque: [],
178
+ secrets: [],
179
+ unwritable: [
180
+ ...SERVER_OWNED,
181
+ ADDRESS,
182
+ // Derived from `deleted_at` / `suspended_at`; there is no column and no
183
+ // write path. See `platformWorkspaceLifecycleFields`.
184
+ 'status',
185
+ 'deletedAt',
186
+ 'purgeAfter',
187
+ // `deletedAt + 30 days`'s sibling: set only by `createWorkspace`, and
188
+ // only together with `sandbox`, which the DB enforces with a CHECK.
189
+ 'sandboxExpiresAt',
190
+ // Always `free` on create. There is no plan parameter on this surface.
191
+ 'plan',
192
+ // The detail route's nested reads. A document declares `users:` as its
193
+ // own section; copying the read shape in would describe nothing.
194
+ 'users',
195
+ 'channels',
196
+ 'counts',
197
+ ],
198
+ },
199
+ {
200
+ type: 'users',
201
+ label: 'member',
202
+ naturalKey: ADDRESS,
203
+ keySource: 'authored',
204
+ /*
205
+ * FALSE. `memberships` has no unique index on (workspace_id, user_id) that
206
+ * this file can rely on, and more to the point the key here is an EMAIL
207
+ * resolved to a user — `provisionUser` creates the account or attaches an
208
+ * existing one. Two rows with one email in one workspace is not a shape
209
+ * the server produces today, and `diff.ts` refuses to touch either if it
210
+ * ever does, which is the only honest answer.
211
+ */
212
+ keyEnforced: false,
213
+ /*
214
+ * Case-folded, because the server folds it: `provisionUser` lowercases
215
+ * the email before it looks the account up
216
+ * (`platform-provisioning.service.ts`), so `Ops@Acme.test` in a document
217
+ * IS `ops@acme.test` on the server. Without this the row plans as a
218
+ * create plus an orphan on every run. The slug half is already lowercase
219
+ * by the slug rule, so folding the whole address is safe.
220
+ */
221
+ normalizeKey: (key) => key.toLowerCase(),
222
+ /*
223
+ * The DETAIL route, not a list route, because there is no list route.
224
+ *
225
+ * `GET /v1/platform/workspaces/{workspaceId}` answers `users[]` with
226
+ * `{ userId, email, role }` and nothing else. `rowsFrom` pulls them out;
227
+ * `platform-live.ts` fetches each path once and reuses it, so declaring
228
+ * two resources against one route costs one request.
229
+ */
230
+ list: { path: '/v1/platform/workspaces/{workspaceId}', paged: false },
231
+ rowsFrom: (response) => {
232
+ const users = response?.users;
233
+ return Array.isArray(users) ? users : [];
234
+ },
235
+ /*
236
+ * `userId`, because that is what the DELETE path takes
237
+ * (`…/users/{userId}`) and what the detail route returns. There is no
238
+ * membership id on the read — `provisionUser`'s RESPONSE carries
239
+ * `membershipId`, the detail does not — so a membership cannot be
240
+ * addressed from a plan and nothing here tries to.
241
+ */
242
+ idField: 'userId',
243
+ create: { path: '/v1/platform/workspaces/{workspaceId}/users' },
244
+ update: [],
245
+ updateBlocked: "there is no route that changes a member's role. `POST …/users` creates or attaches and " +
246
+ 'then sets the role once; after that the only platform write is removal. Change a role ' +
247
+ 'from inside the workspace (`PATCH /v1/workspace/team/{id}` with a `ck_` key), or remove ' +
248
+ 'the row from this document, apply, add it back with the new role and apply again — which ' +
249
+ 'deletes and recreates the MEMBERSHIP and keeps the user account.',
250
+ // 200 `{ revoked }`. A soft delete on the membership; the user account and
251
+ // every historical assignee reference survive, which is why this is safe
252
+ // to prune.
253
+ remove: { path: '/v1/platform/workspaces/{workspaceId}/users/{id}' },
254
+ /*
255
+ * `name` is WRITE-ONLY on this surface. `provisionUserSchema` requires it;
256
+ * the detail route returns `userId`, `email` and `role` and no name at
257
+ * all. Without this a document that sets a name reports a change on every
258
+ * run and — there being no update route — is permanently blocked.
259
+ */
260
+ opaque: ['name'],
261
+ secrets: [],
262
+ unwritable: [...SERVER_OWNED, ADDRESS, 'userId', 'membershipId', 'created'],
263
+ references: {
264
+ /*
265
+ * `workspace: "acme"` — the slug, resolved to the uuid that goes in the
266
+ * PATH. `pathParam` is what keeps it out of the body: `provisionUserSchema`
267
+ * is a plain `z.object`, so a `workspaceId` field would be stripped in
268
+ * silence and the route would answer 201 having ignored it.
269
+ */
270
+ workspace: { type: 'workspaces', wire: 'workspaceId', pathParam: true },
271
+ },
272
+ parent: { type: 'workspaces', field: 'workspace', wire: 'workspaceId', childKey: 'email' },
273
+ },
274
+ {
275
+ type: 'apiKeys',
276
+ label: 'workspace API key',
277
+ naturalKey: ADDRESS,
278
+ keySource: 'authored',
279
+ // `api_keys` has no unique index on `name`. This file is the only thing
280
+ // keeping one key per name, so a duplicate is refused rather than guessed.
281
+ keyEnforced: false,
282
+ list: { path: '/v1/platform/workspaces/{workspaceId}/api_keys', paged: false },
283
+ idField: 'id',
284
+ create: { path: '/v1/platform/workspaces/{workspaceId}/api_keys' },
285
+ update: [],
286
+ updateBlocked: "a key's `scopes` and `expiresAt` are fixed at mint — there is no PATCH. Widening a key " +
287
+ 'means minting a new one and revoking the old one, which is a NEW `ck_` token your ' +
288
+ 'deployment has to pick up, so this CLI will not do it behind a diff. Change the `name` in ' +
289
+ 'the document instead: the new key is minted and the old one is reported as an orphan.',
290
+ // 200 `{ revoked }`, idempotent — `false` means it was already off.
291
+ remove: { path: '/v1/platform/workspaces/{workspaceId}/api_keys/{id}' },
292
+ opaque: [],
293
+ /*
294
+ * The token, shown exactly once. `apply.ts` prints it in a block of its
295
+ * own and writes it nowhere — not into the document, which is the one file
296
+ * we KNOW is in git.
297
+ */
298
+ secrets: ['token'],
299
+ unwritable: [
300
+ ...SERVER_OWNED,
301
+ ADDRESS,
302
+ 'prefix',
303
+ 'lastUsedAt',
304
+ 'revokedAt',
305
+ // Response-only: the sentence telling the caller to copy the token.
306
+ 'note',
307
+ 'token',
308
+ ],
309
+ /*
310
+ * A revoked key stays in the list with `revokedAt` set, by that route's
311
+ * own documented contract. See the header, point 2.
312
+ */
313
+ ignoreLive: revoked,
314
+ references: {
315
+ workspace: { type: 'workspaces', wire: 'workspaceId', pathParam: true },
316
+ },
317
+ parent: { type: 'workspaces', field: 'workspace', wire: 'workspaceId', childKey: 'name' },
318
+ },
319
+ {
320
+ type: 'botCredentials',
321
+ label: 'bot credential',
322
+ naturalKey: ADDRESS,
323
+ keySource: 'authored',
324
+ keyEnforced: false,
325
+ list: { path: '/v1/platform/workspaces/{workspaceId}/bots', paged: false },
326
+ idField: 'id',
327
+ create: { path: '/v1/platform/workspaces/{workspaceId}/bots' },
328
+ update: [],
329
+ updateBlocked: 'there is no PATCH on a bot credential. What it can do is fixed — the operations on ' +
330
+ '`PlatformBotController`, inside one workspace — so there is no scope to widen and ' +
331
+ 'nothing to change but the expiry, which is set at mint. Rename it in the document to ' +
332
+ 'mint a replacement and orphan the old one.',
333
+ // 200 `{ revoked }`. Effective on the next request.
334
+ remove: { path: '/v1/platform/workspaces/{workspaceId}/bots/{id}' },
335
+ opaque: [],
336
+ secrets: ['token'],
337
+ unwritable: [
338
+ ...SERVER_OWNED,
339
+ ADDRESS,
340
+ 'prefix',
341
+ 'createdByPlatformAppId',
342
+ // Set when a bot credential is bound to an `ai_bots` row, which is not
343
+ // something `POST …/bots` accepts.
344
+ 'aiBotId',
345
+ 'lastUsedAt',
346
+ 'revokedAt',
347
+ 'note',
348
+ 'token',
349
+ ],
350
+ ignoreLive: revoked,
351
+ references: {
352
+ workspace: { type: 'workspaces', wire: 'workspaceId', pathParam: true },
353
+ },
354
+ parent: { type: 'workspaces', field: 'workspace', wire: 'workspaceId', childKey: 'name' },
355
+ },
356
+ {
357
+ type: 'knowledgeSources',
358
+ label: 'knowledge source',
359
+ naturalKey: ADDRESS,
360
+ keySource: 'authored',
361
+ /*
362
+ * FALSE, and `label` must therefore be written explicitly. It DEFAULTS to
363
+ * the generated collection name when omitted, so a document that leaves
364
+ * it out would be matching on a value the server chose — which is the
365
+ * same trap `docsSources` carries in the workspace table.
366
+ */
367
+ keyEnforced: false,
368
+ list: {
369
+ path: '/v1/platform/workspaces/{workspaceId}/knowledge_sources',
370
+ paged: false,
371
+ },
372
+ idField: 'id',
373
+ create: { path: '/v1/platform/workspaces/{workspaceId}/knowledge_sources' },
374
+ update: [],
375
+ updateBlocked: 'there is no PATCH on a platform knowledge source. `url`, the crawl bounds and the ' +
376
+ 'publish flag are fixed at create. Disconnect it and connect a new one — ' +
377
+ '`DELETE …/knowledge_sources/{id}` KEEPS every article the source imported, so nothing ' +
378
+ 'the bot has been answering from is lost — or give the new one a different `label`.',
379
+ // 200 `{ deleted: true }`. Stops the re-crawl and KEEPS the articles,
380
+ // which is what makes this safe to prune.
381
+ remove: { path: '/v1/platform/workspaces/{workspaceId}/knowledge_sources/{id}' },
382
+ /*
383
+ * Accepted on create and returned by nothing. `KNOWLEDGE_SOURCE_FIELDS`
384
+ * has no `maxPages`, `maxDepth`, `confineToPathPrefix` or
385
+ * `followPdfLinks` — the worker clamps and applies them and the read
386
+ * shape never mentions them. Declared opaque so a document may set them
387
+ * on a create without reporting a phantom change for ever afterwards.
388
+ */
389
+ opaque: ['maxPages', 'maxDepth', 'confineToPathPrefix', 'followPdfLinks'],
390
+ secrets: [],
391
+ unwritable: [
392
+ ...SERVER_OWNED,
393
+ ADDRESS,
394
+ // Projected into `url` below; the document writes `url`.
395
+ 'seedUrl',
396
+ 'status',
397
+ 'syncStatus',
398
+ 'lastSyncedAt',
399
+ 'lastError',
400
+ 'articleCount',
401
+ 'unreadableCount',
402
+ // `POST` answers the resource plus the literal `true`.
403
+ 'queued',
404
+ ],
405
+ /*
406
+ * `seedUrl` → `url`. The write takes `url`; the read answers `seedUrl`.
407
+ *
408
+ * `publishOnIngest` is left exactly as it comes: it is the ALIAS, both
409
+ * names are accepted on the way in and both come back always equal, so a
410
+ * document may use either and the diff compares like with like.
411
+ */
412
+ project: (row) => ({ ...row, url: row['seedUrl'] }),
413
+ references: {
414
+ workspace: { type: 'workspaces', wire: 'workspaceId', pathParam: true },
415
+ },
416
+ parent: { type: 'workspaces', field: 'workspace', wire: 'workspaceId', childKey: 'label' },
417
+ },
418
+ {
419
+ /*
420
+ * A tenant's business hours, routing and web-channel config — one row per
421
+ * workspace, `{ "workspace": "acme", "businessHours": {…}, "routing": {…},
422
+ * "widgetChannel": { "config": {…} } }`, every section optional.
423
+ *
424
+ * A SINGLETON: `childKey` is the parent field itself, so the address is
425
+ * `acme/acme` and `platform-live.ts` reads it only for workspaces this
426
+ * section names. All three sections are MERGED by the server, so
427
+ * `mergeFields` compares only the keys the document writes — a field left
428
+ * out is left as stored, on the server and in the plan alike.
429
+ *
430
+ * `widgetChannel.config.allowedOrigins` is the exact origin list (the
431
+ * config merge replaces a list). `widgetChannel.origins` only ever ADDS
432
+ * (`planWidgetChannelWrite` unions), so it is projected for reading but a
433
+ * shorter list there can never verify.
434
+ */
435
+ type: 'settings',
436
+ label: 'workspace settings',
437
+ naturalKey: ADDRESS,
438
+ keySource: 'authored',
439
+ // One per workspace by construction: there is nothing to duplicate.
440
+ keyEnforced: true,
441
+ list: { path: '/v1/platform/workspaces/{workspaceId}/settings', paged: false },
442
+ rowsFrom: (response) => typeof response === 'object' && response !== null && !Array.isArray(response)
443
+ ? [response]
444
+ : [],
445
+ idField: 'workspaceId',
446
+ // A POST that MERGES into settings every workspace already has — the
447
+ // `widget_channel` shape. On a workspace this run creates it is the same call.
448
+ create: { path: '/v1/platform/workspaces/{workspaceId}/settings' },
449
+ update: [{ method: 'POST', path: '/v1/platform/workspaces/{workspaceId}/settings' }],
450
+ remove: null,
451
+ removeBlocked: "a workspace's settings cannot be deleted. Take the row out of the document to stop " +
452
+ 'managing them; they stay as stored.',
453
+ mergeFields: ['businessHours', 'routing', 'widgetChannel'],
454
+ opaque: [],
455
+ secrets: [],
456
+ unwritable: [...SERVER_OWNED, ADDRESS],
457
+ project: (row) => {
458
+ const wc = row['widgetChannel'];
459
+ const config = wc?.['config'];
460
+ return wc === null || wc === undefined
461
+ ? row
462
+ : { ...row, widgetChannel: { ...wc, origins: config?.['allowedOrigins'] ?? [] } };
463
+ },
464
+ references: {
465
+ workspace: { type: 'workspaces', wire: 'workspaceId', pathParam: true },
466
+ },
467
+ parent: { type: 'workspaces', field: 'workspace', wire: 'workspaceId', childKey: 'workspace' },
468
+ },
469
+ ];
470
+ export const PLATFORM_RESOURCE_TYPES = PLATFORM_RESOURCES.map((r) => r.type);
471
+ const BY_TYPE = new Map(PLATFORM_RESOURCES.map((r) => [r.type, r]));
472
+ export function platformResourceDef(type) {
473
+ return BY_TYPE.get(type);
474
+ }
475
+ /**
476
+ * Keys a platform document may NOT declare, refused by name with the reason
477
+ * rather than reported as "unknown resource type", which would send the
478
+ * reader hunting for a spelling mistake (`wac/document.ts` does the same for
479
+ * `channels:`).
480
+ *
481
+ * Three are CHT-F04's old top-level names, kept as pointers to the `settings`
482
+ * row that now plans them. The fourth is real: **a login link is a mint, not
483
+ * a declaration.** Its plaintext exists in one response and nowhere else,
484
+ * `GET /v1/platform/login_links` returns no token and no hash, and a document
485
+ * that "declared" one would mint a new single-use credential on every apply.
486
+ */
487
+ export const REFUSED_KEYS = {
488
+ widgetChannel: 'declare it inside a `settings` row: `{ "workspace": "acme", "widgetChannel": { "config": ' +
489
+ '{ … } } }`. It is read back by `GET /v1/platform/workspaces/{id}/settings` and merged by ' +
490
+ 'the POST beside it.',
491
+ businessHours: 'declare it inside a `settings` row: `{ "workspace": "acme", "businessHours": { "tz": ' +
492
+ '"Europe/Madrid", … } }`.',
493
+ routing: 'declare it inside a `settings` row: `{ "workspace": "acme", "routing": { ' +
494
+ '"defaultStrategy": "round_robin" } }`.',
495
+ loginLinks: 'a login link is a single-use credential, not a declaration. Its plaintext exists in the ' +
496
+ 'mint response and nowhere else, `GET /v1/platform/login_links` carries neither token nor ' +
497
+ 'hash, and anything declarative would mint a fresh one on every apply. Use `chatly api ' +
498
+ 'POST /v1/platform/login_links`.',
499
+ };
500
+ /** Every key `resources:` accepts, including the ones it refuses by name. */
501
+ export const PLATFORM_RESOURCE_KEYS = [
502
+ ...PLATFORM_RESOURCE_TYPES,
503
+ ...Object.keys(REFUSED_KEYS),
504
+ ];
505
+ /** The address of one child row, for a message or a key. */
506
+ export function childAddress(parentKey, childKey) {
507
+ return addressOf(parentKey, childKey);
508
+ }
509
+ //# sourceMappingURL=platform-resources.js.map