@jini-ai/cms 0.5.2 → 0.5.4

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 (261) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/forms/delete-submission.d.ts +38 -0
  46. package/dist/forms/delete-submission.js +25 -0
  47. package/dist/forms/duplicate-slug.d.ts +81 -0
  48. package/dist/forms/duplicate-slug.js +115 -0
  49. package/dist/forms/errors.d.ts +53 -0
  50. package/dist/forms/errors.js +25 -0
  51. package/dist/forms/express/handler.d.ts +59 -0
  52. package/dist/forms/express/handler.js +217 -0
  53. package/dist/forms/express/index.d.ts +2 -0
  54. package/dist/forms/express/index.js +2 -0
  55. package/dist/forms/fields-json.d.ts +14 -0
  56. package/dist/forms/fields-json.js +11 -0
  57. package/dist/forms/forms.d.ts +59 -0
  58. package/dist/forms/forms.js +213 -0
  59. package/dist/forms/html/authoring-adapter.d.ts +34 -0
  60. package/dist/forms/html/authoring-adapter.js +48 -0
  61. package/dist/forms/html/html-authoring.d.ts +12 -0
  62. package/dist/forms/html/html-authoring.js +133 -0
  63. package/dist/forms/html/html-submission-adapter.d.ts +10 -0
  64. package/dist/forms/html/html-submission-adapter.js +15 -0
  65. package/dist/forms/html/index.d.ts +4 -0
  66. package/dist/forms/html/index.js +4 -0
  67. package/dist/forms/html-render.d.ts +33 -0
  68. package/dist/forms/html-render.js +31 -0
  69. package/dist/forms/index.d.ts +17 -0
  70. package/dist/forms/index.js +17 -0
  71. package/dist/forms/notify-subscriber.d.ts +32 -0
  72. package/dist/forms/notify-subscriber.js +60 -0
  73. package/dist/forms/ports.d.ts +168 -0
  74. package/dist/forms/ports.js +2 -0
  75. package/dist/forms/rate-limit-key.d.ts +8 -0
  76. package/dist/forms/rate-limit-key.js +7 -0
  77. package/dist/forms/repo.memory.d.ts +120 -0
  78. package/dist/forms/repo.memory.js +188 -0
  79. package/dist/forms/sql/index.d.ts +5 -0
  80. package/dist/forms/sql/index.js +5 -0
  81. package/dist/forms/sql/repo.d.ts +94 -0
  82. package/dist/forms/sql/repo.js +175 -0
  83. package/dist/forms/sql/repo.rows.d.ts +33 -0
  84. package/dist/forms/sql/repo.rows.js +64 -0
  85. package/dist/forms/sql/submission-ip-retention-repo.d.ts +9 -0
  86. package/dist/forms/sql/submission-ip-retention-repo.js +40 -0
  87. package/dist/forms/sql/tables.d.ts +32 -0
  88. package/dist/forms/sql/tables.js +2 -0
  89. package/dist/forms/submission-attempts.d.ts +70 -0
  90. package/dist/forms/submission-attempts.js +70 -0
  91. package/dist/forms/submission-email-validation.d.ts +8 -0
  92. package/dist/forms/submission-email-validation.js +34 -0
  93. package/dist/forms/submission-ip-retention.d.ts +30 -0
  94. package/dist/forms/submission-ip-retention.js +27 -0
  95. package/dist/forms/submit-service.d.ts +78 -0
  96. package/dist/forms/submit-service.js +109 -0
  97. package/dist/forms/types.d.ts +49 -0
  98. package/dist/forms/types.js +2 -0
  99. package/dist/forms/write-service.d.ts +83 -0
  100. package/dist/forms/write-service.js +271 -0
  101. package/dist/media/index.d.ts +0 -3
  102. package/dist/media/index.js +1 -4
  103. package/dist/media/node.d.ts +5 -0
  104. package/dist/media/node.js +6 -0
  105. package/dist/redirects/capture.d.ts +80 -0
  106. package/dist/redirects/capture.js +66 -0
  107. package/dist/redirects/express/index.d.ts +27 -0
  108. package/dist/redirects/express/index.js +40 -0
  109. package/dist/redirects/hit-sink.d.ts +56 -0
  110. package/dist/redirects/hit-sink.js +44 -0
  111. package/dist/redirects/host-config/index.d.ts +40 -0
  112. package/dist/redirects/host-config/index.js +145 -0
  113. package/dist/redirects/index.d.ts +14 -0
  114. package/dist/redirects/index.js +14 -0
  115. package/dist/redirects/matcher.d.ts +58 -0
  116. package/dist/redirects/matcher.js +144 -0
  117. package/dist/redirects/order.d.ts +30 -0
  118. package/dist/redirects/order.js +26 -0
  119. package/dist/redirects/ports.d.ts +242 -0
  120. package/dist/redirects/ports.internal.d.ts +57 -0
  121. package/dist/redirects/ports.internal.js +14 -0
  122. package/dist/redirects/ports.js +2 -0
  123. package/dist/redirects/redirects.d.ts +226 -0
  124. package/dist/redirects/redirects.js +590 -0
  125. package/dist/redirects/referrer-alias.d.ts +31 -0
  126. package/dist/redirects/referrer-alias.js +33 -0
  127. package/dist/redirects/repo.memory.d.ts +51 -0
  128. package/dist/redirects/repo.memory.js +98 -0
  129. package/dist/redirects/reserved-destination.d.ts +58 -0
  130. package/dist/redirects/reserved-destination.js +59 -0
  131. package/dist/redirects/resolver.d.ts +100 -0
  132. package/dist/redirects/resolver.js +87 -0
  133. package/dist/redirects/spa/index.d.ts +30 -0
  134. package/dist/redirects/spa/index.js +27 -0
  135. package/dist/redirects/sql/index.d.ts +4 -0
  136. package/dist/redirects/sql/index.js +4 -0
  137. package/dist/redirects/sql/repo.d.ts +82 -0
  138. package/dist/redirects/sql/repo.js +143 -0
  139. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  140. package/dist/redirects/sql/repo.rows.js +79 -0
  141. package/dist/redirects/sql/row-types.d.ts +34 -0
  142. package/dist/redirects/sql/row-types.js +2 -0
  143. package/dist/redirects/target-gate.d.ts +30 -0
  144. package/dist/redirects/target-gate.js +85 -0
  145. package/dist/redirects/types.d.ts +213 -0
  146. package/dist/redirects/types.js +42 -0
  147. package/dist/seo/absolute-url.d.ts +58 -0
  148. package/dist/seo/absolute-url.js +117 -0
  149. package/dist/seo/dom/index.d.ts +14 -0
  150. package/dist/seo/dom/index.js +52 -0
  151. package/dist/seo/errors.d.ts +35 -0
  152. package/dist/seo/errors.js +35 -0
  153. package/dist/seo/feed.d.ts +49 -0
  154. package/dist/seo/feed.js +61 -0
  155. package/dist/seo/index.d.ts +13 -0
  156. package/dist/seo/index.js +13 -0
  157. package/dist/seo/media.d.ts +57 -0
  158. package/dist/seo/media.js +134 -0
  159. package/dist/seo/page-head.d.ts +18 -0
  160. package/dist/seo/page-head.js +114 -0
  161. package/dist/seo/ports.d.ts +221 -0
  162. package/dist/seo/ports.js +27 -0
  163. package/dist/seo/seo.d.ts +73 -0
  164. package/dist/seo/seo.js +230 -0
  165. package/dist/seo/sitemap.d.ts +139 -0
  166. package/dist/seo/sitemap.js +217 -0
  167. package/dist/seo/types.d.ts +278 -0
  168. package/dist/seo/types.js +2 -0
  169. package/dist/seo/write-service.d.ts +79 -0
  170. package/dist/seo/write-service.js +273 -0
  171. package/dist/seo/xml-escape.d.ts +16 -0
  172. package/dist/seo/xml-escape.js +19 -0
  173. package/dist/settings/express/cms-adapter.d.ts +20 -0
  174. package/dist/settings/express/cms-adapter.js +124 -0
  175. package/dist/settings/express/contracts.d.ts +135 -0
  176. package/dist/settings/express/contracts.js +2 -0
  177. package/dist/settings/express/http.d.ts +10 -0
  178. package/dist/settings/express/http.js +211 -0
  179. package/dist/settings/express/index.d.ts +5 -0
  180. package/dist/settings/express/index.js +5 -0
  181. package/dist/settings/express/shared.d.ts +68 -0
  182. package/dist/settings/express/shared.js +54 -0
  183. package/dist/settings/express/stream.d.ts +24 -0
  184. package/dist/settings/express/stream.js +147 -0
  185. package/dist/settings/index.d.ts +3 -38
  186. package/dist/settings/index.js +3 -35
  187. package/dist/taxonomy/write-service.d.ts +2 -30
  188. package/dist/widgets/authorize-helper.d.ts +42 -0
  189. package/dist/widgets/authorize-helper.js +16 -0
  190. package/dist/widgets/concurrency.d.ts +12 -0
  191. package/dist/widgets/concurrency.js +44 -0
  192. package/dist/widgets/config-validation.d.ts +34 -0
  193. package/dist/widgets/config-validation.js +125 -0
  194. package/dist/widgets/embed-service.d.ts +133 -0
  195. package/dist/widgets/embed-service.js +460 -0
  196. package/dist/widgets/embed-validation.d.ts +23 -0
  197. package/dist/widgets/embed-validation.js +38 -0
  198. package/dist/widgets/entry-payload.d.ts +103 -0
  199. package/dist/widgets/entry-payload.js +160 -0
  200. package/dist/widgets/errors.d.ts +136 -0
  201. package/dist/widgets/errors.js +136 -0
  202. package/dist/widgets/host-ports.d.ts +149 -0
  203. package/dist/widgets/host-ports.js +2 -0
  204. package/dist/widgets/html/html-embeds.d.ts +247 -0
  205. package/dist/widgets/html/html-embeds.js +408 -0
  206. package/dist/widgets/html/index.d.ts +5 -0
  207. package/dist/widgets/html/index.js +4 -0
  208. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  209. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  210. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  211. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  212. package/dist/widgets/index.d.ts +20 -0
  213. package/dist/widgets/index.js +19 -0
  214. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  215. package/dist/widgets/markers/html-attributes.js +216 -0
  216. package/dist/widgets/markers/index.d.ts +3 -0
  217. package/dist/widgets/markers/index.js +3 -0
  218. package/dist/widgets/markers/marker.d.ts +496 -0
  219. package/dist/widgets/markers/marker.js +780 -0
  220. package/dist/widgets/page-embed-types.d.ts +42 -0
  221. package/dist/widgets/page-embed-types.js +42 -0
  222. package/dist/widgets/ports.d.ts +142 -0
  223. package/dist/widgets/ports.js +2 -0
  224. package/dist/widgets/read-service.d.ts +81 -0
  225. package/dist/widgets/read-service.js +79 -0
  226. package/dist/widgets/region-area-service.d.ts +110 -0
  227. package/dist/widgets/region-area-service.js +180 -0
  228. package/dist/widgets/registry.d.ts +217 -0
  229. package/dist/widgets/registry.js +172 -0
  230. package/dist/widgets/repo.memory.d.ts +41 -0
  231. package/dist/widgets/repo.memory.js +48 -0
  232. package/dist/widgets/resolver-service.d.ts +209 -0
  233. package/dist/widgets/resolver-service.js +909 -0
  234. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  235. package/dist/widgets/resolvers/contact-form.js +54 -0
  236. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  237. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  238. package/dist/widgets/resolvers/index.d.ts +65 -0
  239. package/dist/widgets/resolvers/index.js +155 -0
  240. package/dist/widgets/resolvers/menu.d.ts +42 -0
  241. package/dist/widgets/resolvers/menu.js +43 -0
  242. package/dist/widgets/resolvers/ports.d.ts +82 -0
  243. package/dist/widgets/resolvers/ports.js +2 -0
  244. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  245. package/dist/widgets/resolvers/recent-entries.js +165 -0
  246. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  247. package/dist/widgets/resolvers/taxonomy.js +34 -0
  248. package/dist/widgets/sql/index.d.ts +3 -0
  249. package/dist/widgets/sql/index.js +3 -0
  250. package/dist/widgets/sql/repo.d.ts +41 -0
  251. package/dist/widgets/sql/repo.js +82 -0
  252. package/dist/widgets/sql/tables.d.ts +13 -0
  253. package/dist/widgets/sql/tables.js +2 -0
  254. package/dist/widgets/types.d.ts +213 -0
  255. package/dist/widgets/types.js +17 -0
  256. package/dist/widgets/where-used.d.ts +41 -0
  257. package/dist/widgets/where-used.js +11 -0
  258. package/dist/widgets/write-service.d.ts +204 -0
  259. package/dist/widgets/write-service.js +332 -0
  260. package/dist/workspace/delete.d.ts +1 -1
  261. package/package.json +220 -12
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @file The one HTML/XML text escaper. A leaf module with NO imports, on purpose: the old copies
3
+ * lived inside `render.ts`, `form-render.ts` and `static-render.ts` and were duplicated rather than
4
+ * shared because importing them would have closed a runtime import cycle. A module that imports
5
+ * nothing can be imported from anywhere.
6
+ *
7
+ * `&` is replaced first: it is the escape character for every entity after it, so replacing any
8
+ * other character first would double-escape the `&` that replacement introduced. The apostrophe
9
+ * becomes the numeric `'` in HTML, because `'` is undefined in HTML4/XHTML1.
10
+ */
11
+ const XML_ENTITIES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" };
12
+ const SPECIAL = /[&<>"']/g;
13
+ /** {@link escapeHtml} with the XML named entity `&apos;` for the apostrophe (sitemap and feed XML).
14
+ * @complexity O(text length). */
15
+ // escapeHtml stays with the host renderer. A shared core owner for this leaf is a coordinator follow-up.
16
+ export function escapeXml(required, _optional = {}) {
17
+ return required.text.replace(SPECIAL, (char) => XML_ENTITIES[char] ?? char);
18
+ }
19
+ //# sourceMappingURL=xml-escape.js.map
@@ -0,0 +1,20 @@
1
+ import { type SettingsWriteServiceDeps, type SettingsRepoPort } from "../index.js";
2
+ import type { SettingsService, SettingsPermissions, SettingsChangeFeed } from "./contracts.js";
3
+ /** Keep the CMS write chokepoint while translating its permission vocabulary to host policy. */
4
+ /** Bind the CMS ledger and write chokepoint without copying their domain logic.
5
+ * Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
6
+ * @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
7
+ * @example createCmsSettingsService({ deps: cmsPorts, permissions });
8
+ */
9
+ export declare function createCmsSettingsService(required: {
10
+ deps: SettingsWriteServiceDeps;
11
+ permissions: SettingsPermissions;
12
+ }): SettingsService;
13
+ /** Adapt the ledger through CMS tenant/principal disclosure rules, returning names and cursors only.
14
+ * @complexity O(1) setup; collect is O(page size), with namespace lookup per visible revision.
15
+ * @example createCmsSettingsChangeFeed({ repo });
16
+ */
17
+ export declare function createCmsSettingsChangeFeed(required: {
18
+ repo: SettingsRepoPort;
19
+ }): SettingsChangeFeed;
20
+ //# sourceMappingURL=cms-adapter.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { getEffective, resolveDefinition, set, clear, resetNamespace, registerDefinitions, NON_REGISTER_DEFINITION_OPS, parseNonRegisterDefinitionOp, collectChangedNamespaces, } from "../index.js";
2
+ /** Keep the CMS write chokepoint while translating its permission vocabulary to host policy. */
3
+ /** Bind the CMS ledger and write chokepoint without copying their domain logic.
4
+ * Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
5
+ * @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
6
+ * @example createCmsSettingsService({ deps: cmsPorts, permissions });
7
+ */
8
+ export function createCmsSettingsService(required) {
9
+ const { deps: original, permissions } = required;
10
+ const permissionMap = new Map([
11
+ ["settings.definitions.manage", permissions.manageDefinitions],
12
+ ["settings.global.write", permissions.writeGlobal],
13
+ ["settings.workspace.write", permissions.writeWorkspace],
14
+ ["settings.user.self.write", permissions.writeUserSelf],
15
+ ["settings.user.write", permissions.writeUserOther],
16
+ ["settings.reset.global", permissions.reset.global],
17
+ ["settings.reset.workspace", permissions.reset.workspace],
18
+ ["settings.reset.user", permissions.reset.user],
19
+ ]);
20
+ const deps = { ...original,
21
+ authorize: (input) => {
22
+ const permission = permissionMap.get(input.permission);
23
+ if (permission === undefined)
24
+ return Promise.resolve({ allowed: false, reason: "unknown_settings_permission" });
25
+ return original.authorize({ ...input, permission });
26
+ },
27
+ };
28
+ const repo = deps.repo;
29
+ const listDefinitions = async ({ workspaceId }) => {
30
+ const [platform, workspace] = await Promise.all([
31
+ repo.listActiveDefinitions({ workspaceId: null }), repo.listActiveDefinitions({ workspaceId }),
32
+ ]);
33
+ return [...platform, ...workspace];
34
+ };
35
+ return {
36
+ listDefinitions,
37
+ effective: async ({ namespace, workspaceId, principalId }) => {
38
+ const defs = await listDefinitions({ workspaceId });
39
+ const keys = new Set(defs.filter((d) => d.namespace === namespace).map((d) => d.key));
40
+ const data = [];
41
+ for (const key of keys) {
42
+ const result = await getEffective({ repo }, { namespace, key, scopeContext: { workspaceId, principalId } });
43
+ if (result)
44
+ data.push({ key, value: result.value, sourceLayer: result.sourceLayer, defVersion: result.defVersion });
45
+ }
46
+ return data;
47
+ },
48
+ raw: async ({ namespace, key, workspaceId, principalId }) => {
49
+ const definition = await resolveDefinition({ repo }, { namespace, key, workspaceId });
50
+ if (!definition)
51
+ return null;
52
+ const settingId = definition.settingId;
53
+ const records = await Promise.all([
54
+ repo.getGlobalValue(settingId), repo.getWorkspaceValue({ workspaceId, settingId }),
55
+ principalId ? repo.getUserValue({ workspaceId, principalId, settingId }) : Promise.resolve(null),
56
+ ]);
57
+ const values = records.map((r) => r?.state === "set" ? r.valueJson : null);
58
+ return { key: `${namespace}.${key}`, global: values[0], workspace: values[1], user: values[2], default: definition.defaultValue };
59
+ },
60
+ set: (input) => set({ deps, input }),
61
+ clear: (input) => clear({ deps, input }),
62
+ reset: async (input) => {
63
+ const definitions = await repo.listActiveDefinitions({ workspaceId: input.scope === "global" ? null : (input.workspaceId ?? input.authWorkspaceId ?? null) });
64
+ const keys = definitions.filter((d) => d.namespace === input.namespace).map((d) => d.key);
65
+ return resetNamespace({ deps, input }, keys);
66
+ },
67
+ definitions: ({ items, callerPrincipalId, authWorkspaceId }) => applyDefinitions({ deps, items, callerPrincipalId, authWorkspaceId }),
68
+ };
69
+ }
70
+ /** Preserve register batching and ordered non-register writes over existing CMS operation contracts.
71
+ * Unknown operations return their name. Earlier non-register writes are not rolled back by later failures.
72
+ * @complexity O(item count) time/space plus CMS effects; this batch is not atomic.
73
+ */
74
+ async function applyDefinitions(required) {
75
+ const { deps, items, callerPrincipalId, authWorkspaceId } = required;
76
+ const toRegister = [];
77
+ const applied = [];
78
+ for (const raw of items) {
79
+ const item = raw;
80
+ const op = String(item.op ?? "register");
81
+ const namespace = String(item.namespace ?? "");
82
+ const key = String(item.key ?? "");
83
+ const ownerKind = item.ownerKind;
84
+ const workspaceId = ownerKind === "site" ? authWorkspaceId : null;
85
+ const defaultValue = (item.defaultJson ?? null);
86
+ if (op === "register") {
87
+ toRegister.push({ namespace, key, ownerKind, workspaceId, schema: item.schemaJson,
88
+ defaultValue, scopes: Number(item.scopes), secret: Boolean(item.secret ?? false) });
89
+ }
90
+ else {
91
+ const parsedOp = parseNonRegisterDefinitionOp({ op });
92
+ if (!parsedOp)
93
+ return { unknownOp: op };
94
+ const opItem = { namespace, key, ownerKind, workspaceId, defaultJson: defaultValue,
95
+ ...(item.newNamespace === undefined ? {} : { newNamespace: item.newNamespace }),
96
+ ...(item.newKey === undefined ? {} : { newKey: item.newKey }),
97
+ ...(item.schemaJson === undefined ? {} : { schemaJson: item.schemaJson }),
98
+ ...(item.coercionJson === undefined ? {} : { coercionJson: item.coercionJson }),
99
+ };
100
+ await NON_REGISTER_DEFINITION_OPS[parsedOp]({ ctx: { deps, callerPrincipalId, authWorkspaceId }, item: opItem });
101
+ }
102
+ applied.push({ key: `${namespace}.${key}`, op, status: "applied" });
103
+ }
104
+ if (toRegister.length)
105
+ await registerDefinitions({ deps, input: { definitions: toRegister, callerPrincipalId, authWorkspaceId } });
106
+ return { applied };
107
+ }
108
+ /** Adapt the ledger through CMS tenant/principal disclosure rules, returning names and cursors only.
109
+ * @complexity O(1) setup; collect is O(page size), with namespace lookup per visible revision.
110
+ * @example createCmsSettingsChangeFeed({ repo });
111
+ */
112
+ export function createCmsSettingsChangeFeed(required) {
113
+ const { repo } = required;
114
+ return {
115
+ head: () => repo.maxRevisionSeq(),
116
+ collect: async ({ sinceSeq, limit, viewer }) => {
117
+ const revisions = await repo.listRevisionsSince({ sinceSeq, limit, workspaceId: viewer.workspaceId });
118
+ // Resolve afresh each batch: a rename must never leave a stale namespace cached forever.
119
+ const batch = await collectChangedNamespaces({ revisions, viewer, resolveNamespace: async ({ settingId }) => (await repo.findDefinitionBySettingId({ settingId }))?.namespace ?? null });
120
+ return { ...batch, examinedCount: revisions.length };
121
+ },
122
+ };
123
+ }
124
+ //# sourceMappingURL=cms-adapter.js.map
@@ -0,0 +1,135 @@
1
+ import type { Request, Response, Express } from "express";
2
+ import type { SettingScope, SettingDefinitionRecord, SetValueRequired, ClearValueRequired, ResetNamespaceRequired, ChangeFeedViewer, ChangeFeedBatch } from "../index.js";
3
+ export type Authorize = (required: {
4
+ principalId: string;
5
+ workspaceId: string;
6
+ permission: string;
7
+ entityType: string;
8
+ }) => Promise<{
9
+ allowed: boolean;
10
+ reason: string;
11
+ }>;
12
+ export type PrincipalResolver = (required: {
13
+ request: Request;
14
+ response: Response;
15
+ }) => Promise<{
16
+ id: string;
17
+ } | null> | {
18
+ id: string;
19
+ } | null;
20
+ export interface Scheduler {
21
+ /** Return an idempotent cancellation callback. */
22
+ every(required: {
23
+ intervalMs: number;
24
+ callback: () => void;
25
+ }): () => void;
26
+ }
27
+ export interface SettingsPermissions {
28
+ read: string;
29
+ readRaw: string;
30
+ readDefinitions: string;
31
+ readOtherUser: string;
32
+ manageDefinitions: string;
33
+ writeGlobal: string;
34
+ writeWorkspace: string;
35
+ writeUserSelf: string;
36
+ writeUserOther: string;
37
+ reset: Record<SettingScope, string>;
38
+ }
39
+ export interface SettingsRoutes {
40
+ definitions: string;
41
+ effective: string;
42
+ raw: string;
43
+ value: string;
44
+ reset: string;
45
+ events: string;
46
+ workspaceParam: string;
47
+ }
48
+ export type EffectiveSettingRow = {
49
+ key: string;
50
+ value: unknown;
51
+ sourceLayer: string;
52
+ defVersion: number;
53
+ };
54
+ export type RawSetting = {
55
+ key: string;
56
+ global: unknown;
57
+ workspace: unknown;
58
+ user: unknown;
59
+ default: unknown;
60
+ };
61
+ export type DefinitionResult = {
62
+ applied: Array<{
63
+ key: string;
64
+ op: string;
65
+ status: string;
66
+ }>;
67
+ } | {
68
+ unknownOp: string;
69
+ };
70
+ export interface SettingsService {
71
+ listDefinitions(required: {
72
+ workspaceId: string;
73
+ }): Promise<SettingDefinitionRecord[]>;
74
+ effective(required: {
75
+ namespace: string;
76
+ workspaceId: string;
77
+ principalId: string | undefined;
78
+ }): Promise<EffectiveSettingRow[]>;
79
+ raw(required: {
80
+ namespace: string;
81
+ key: string;
82
+ workspaceId: string;
83
+ principalId: string | undefined;
84
+ }): Promise<RawSetting | null>;
85
+ set(required: SetValueRequired["input"]): Promise<{
86
+ value: unknown;
87
+ revisionSeq: number;
88
+ }>;
89
+ clear(required: ClearValueRequired["input"]): Promise<{
90
+ revisionSeq: number;
91
+ }>;
92
+ reset(required: ResetNamespaceRequired["input"]): Promise<{
93
+ clearedCount: number;
94
+ revisionSeqs: number[];
95
+ }>;
96
+ definitions(required: {
97
+ items: readonly unknown[];
98
+ callerPrincipalId: string;
99
+ authWorkspaceId: string;
100
+ }): Promise<DefinitionResult>;
101
+ }
102
+ export interface SettingsChangeFeed {
103
+ head(required: Record<string, never>): Promise<number>;
104
+ /** Cursor advances across invisible rows. The emitted id MUST use batch.cursor, never head. */
105
+ collect(required: {
106
+ sinceSeq: number;
107
+ limit: number;
108
+ viewer: ChangeFeedViewer;
109
+ }): Promise<ChangeFeedBatch & {
110
+ examinedCount: number;
111
+ }>;
112
+ }
113
+ export interface SettingsHttpRequired {
114
+ app: Pick<Express, "get" | "post" | "put" | "delete">;
115
+ workspaceId: string;
116
+ ready: Promise<void>;
117
+ service: SettingsService;
118
+ changeFeed: SettingsChangeFeed;
119
+ principalResolver: PrincipalResolver;
120
+ authorize: Authorize;
121
+ scheduler: Scheduler;
122
+ routes: SettingsRoutes;
123
+ permissions: SettingsPermissions;
124
+ }
125
+ export interface SettingsHttpOptions {
126
+ pollIntervalMs?: number;
127
+ keepaliveIntervalMs?: number;
128
+ reauthorizeIntervalMs?: number;
129
+ revisionPageSize?: number;
130
+ onError?: (required: {
131
+ operation: string;
132
+ error: unknown;
133
+ }) => void;
134
+ }
135
+ //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1,10 @@
1
+ import type { SettingsHttpRequired, SettingsHttpOptions } from "./contracts.js";
2
+ /** Mount all eight endpoints. The returned disposer closes active feeds and cancels their schedules. */
3
+ /** Injected policy and services preserve the settings HTTP response envelopes.
4
+ * O(1) mounting; read/mutation work is delegated. Feed resources are bounded per connection.
5
+ * @example registerSettingsRoutes({ app, workspaceId, ready, service, changeFeed, principalResolver, authorize, scheduler, routes, permissions });
6
+ */
7
+ export declare function registerSettingsRoutes(required: SettingsHttpRequired, optional?: SettingsHttpOptions): {
8
+ dispose(): void;
9
+ };
10
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1,211 @@
1
+ import { AliasDepthExceededError, DefinitionInvalidError, DefinitionNotFoundError, DefinitionTombstonedError, ForbiddenError, PrincipalNotFoundError, RenameRetypeConflictError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, } from "../index.js";
2
+ import { respondToSettingsError, resolveTargetWorkspaceId, resolveUserLayerReadTarget, settingsWritePermission } from "./shared.js";
3
+ import { openSettingsChangeStream } from "./stream.js";
4
+ const VALUE_ERRORS = [
5
+ { matches: (e) => e instanceof PrincipalNotFoundError, status: 404, code: "PRINCIPAL_NOT_FOUND" },
6
+ { matches: (e) => e instanceof DefinitionNotFoundError, status: 404, code: "DEFINITION_NOT_FOUND" },
7
+ { matches: (e) => e instanceof DefinitionTombstonedError, status: 409, code: "DEFINITION_TOMBSTONED" },
8
+ { matches: (e) => e instanceof ScopeNotAllowedError, status: 400, code: "SCOPE_NOT_ALLOWED" },
9
+ { matches: (e) => e instanceof ValueValidationFailedError, status: 400, code: "VALUE_VALIDATION_FAILED" },
10
+ { matches: (e) => e instanceof ForbiddenError, status: 403, code: "FORBIDDEN" },
11
+ ];
12
+ const DEFINITION_ERRORS = [
13
+ { matches: (e) => e instanceof SecretNotSupportedError, status: 400, code: "SECRET_NOT_SUPPORTED" },
14
+ { matches: (e) => e instanceof DefinitionInvalidError, status: 400, code: "DEFINITION_INVALID" },
15
+ { matches: (e) => e instanceof RenameRetypeConflictError, status: 409, code: "RENAME_RETYPE_CONFLICT" },
16
+ { matches: (e) => e instanceof AliasDepthExceededError, status: 409, code: "ALIAS_DEPTH_EXCEEDED" },
17
+ { matches: (e) => e instanceof DefinitionTombstonedError, status: 409, code: "DEFINITION_TOMBSTONED" },
18
+ { matches: (e) => e instanceof ForbiddenError, status: 403, code: "FORBIDDEN" },
19
+ ];
20
+ const RESET_ERRORS = VALUE_ERRORS.filter((m) => m.code === "FORBIDDEN");
21
+ const scopes = ["global", "workspace", "user"];
22
+ function validation(res, error) {
23
+ res.status(400).json({ error, code: "VALIDATION_ERROR" });
24
+ }
25
+ function denied(res, principalId, permission, reason) {
26
+ res.status(403).json({ error: `principal '${principalId}' is not authorized for '${permission}' (${reason})`,
27
+ code: "FORBIDDEN", details: { permission, reason } });
28
+ }
29
+ function bodyOf(req) {
30
+ return req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
31
+ }
32
+ /** Mount all eight endpoints. The returned disposer closes active feeds and cancels their schedules. */
33
+ /** Injected policy and services preserve the settings HTTP response envelopes.
34
+ * O(1) mounting; read/mutation work is delegated. Feed resources are bounded per connection.
35
+ * @example registerSettingsRoutes({ app, workspaceId, ready, service, changeFeed, principalResolver, authorize, scheduler, routes, permissions });
36
+ */
37
+ export function registerSettingsRoutes(required, optional = {}) {
38
+ const { app, workspaceId, service, routes, permissions } = required;
39
+ const streams = new Set();
40
+ let disposed = false;
41
+ const wrap = (handler, mappings = []) => async (req, res) => {
42
+ if (disposed) {
43
+ res.status(503).json({ error: "settings routes are disposed", code: "UNAVAILABLE" });
44
+ return;
45
+ }
46
+ if (String(req.params[routes.workspaceParam] ?? "") !== workspaceId) {
47
+ res.status(404).json({ error: "workspace was not found" });
48
+ return;
49
+ }
50
+ let principal;
51
+ try {
52
+ await required.ready;
53
+ principal = await required.principalResolver({ request: req, response: res });
54
+ }
55
+ catch {
56
+ // Readiness/principal failures preserve the authentication boundary's existing 401 envelope.
57
+ res.status(401).json({ error: "unauthenticated", code: "UNAUTHENTICATED" });
58
+ return;
59
+ }
60
+ if (!principal) {
61
+ res.status(401).json({ error: "unauthenticated", code: "UNAUTHENTICATED" });
62
+ return;
63
+ }
64
+ try {
65
+ await handler(req, res, principal.id);
66
+ }
67
+ catch (error) {
68
+ if (res.headersSent) {
69
+ // Reporting cannot prevent ending a response whose headers have already been sent.
70
+ try {
71
+ optional.onError?.({ operation: "settings-request", error });
72
+ }
73
+ catch { /* Host diagnostics are best-effort. */ }
74
+ finally {
75
+ res.end();
76
+ }
77
+ }
78
+ else
79
+ respondToSettingsError({ response: res, error, mappings });
80
+ }
81
+ };
82
+ const check = async (res, principalId, permission, entityType) => {
83
+ const result = await required.authorize({ principalId, permission, workspaceId, entityType });
84
+ if (!result.allowed)
85
+ denied(res, principalId, permission, result.reason);
86
+ return result.allowed;
87
+ };
88
+ const readTarget = async (res, principalId, requestedPrincipalId) => {
89
+ const result = await resolveUserLayerReadTarget({ deps: { workspaceId, authorize: required.authorize, permission: permissions.readOtherUser },
90
+ input: { callerPrincipalId: principalId, requestedPrincipalId } });
91
+ if (!result.allowed)
92
+ res.status(403).json({
93
+ error: `principal '${principalId}' is not authorized to read another principal's user-layer value (${result.reason})`,
94
+ code: "FORBIDDEN", details: { permission: permissions.readOtherUser, reason: result.reason },
95
+ });
96
+ return result;
97
+ };
98
+ app.get(routes.definitions, wrap(async (_req, res, principalId) => {
99
+ if (!await check(res, principalId, permissions.readDefinitions, "setting-definition"))
100
+ return;
101
+ const definitions = await service.listDefinitions({ workspaceId });
102
+ const data = definitions.map(({ namespace, key, ownerKind, scopes, status, version }) => ({ namespace, key, ownerKind, scopes, status, version }))
103
+ .sort((a, b) => a.namespace.localeCompare(b.namespace) || a.key.localeCompare(b.key));
104
+ res.json({ data });
105
+ }));
106
+ app.get(routes.effective, wrap(async (req, res, principalId) => {
107
+ if (!await check(res, principalId, permissions.read, "setting-value"))
108
+ return;
109
+ const namespace = String(req.query.namespace ?? "");
110
+ if (!namespace) {
111
+ validation(res, "'namespace' query param is required");
112
+ return;
113
+ }
114
+ const target = await readTarget(res, principalId, req.query.principalId ? String(req.query.principalId) : principalId);
115
+ if (!target.allowed)
116
+ return;
117
+ res.json({ data: await service.effective({ namespace, workspaceId, principalId: target.principalId }) });
118
+ }));
119
+ app.get(routes.raw, wrap(async (req, res, principalId) => {
120
+ if (!await check(res, principalId, permissions.readRaw, "setting-value"))
121
+ return;
122
+ const namespace = String(req.query.namespace ?? "");
123
+ const key = String(req.query.key ?? "");
124
+ if (!namespace || !key) {
125
+ validation(res, "'namespace' and 'key' query params are required");
126
+ return;
127
+ }
128
+ const target = await readTarget(res, principalId, req.query.principalId ? String(req.query.principalId) : undefined);
129
+ if (!target.allowed)
130
+ return;
131
+ const result = await service.raw({ namespace, key, workspaceId, principalId: target.principalId });
132
+ if (!result) {
133
+ res.status(404).json({ error: `definition '${namespace}.${key}' was not found`, code: "DEFINITION_NOT_FOUND" });
134
+ return;
135
+ }
136
+ res.json(result);
137
+ }));
138
+ app.post(routes.definitions, wrap(async (req, res, principalId) => {
139
+ if (!await check(res, principalId, permissions.manageDefinitions, "setting-definition"))
140
+ return;
141
+ const items = bodyOf(req).definitions;
142
+ if (!Array.isArray(items)) {
143
+ validation(res, "'definitions' must be an array");
144
+ return;
145
+ }
146
+ const result = await service.definitions({ items, callerPrincipalId: principalId, authWorkspaceId: workspaceId });
147
+ if ("unknownOp" in result) {
148
+ validation(res, `unknown op '${result.unknownOp}'`);
149
+ return;
150
+ }
151
+ res.json(result);
152
+ }, DEFINITION_ERRORS));
153
+ const mutate = (operation) => async (req, res, callerPrincipalId) => {
154
+ const body = bodyOf(req);
155
+ const namespace = String(body.namespace ?? "");
156
+ const key = String(body.key ?? "");
157
+ const scope = body.scope;
158
+ if (!namespace || !key || !scopes.includes(scope) || (operation === "set" && !("valueJson" in body))) {
159
+ validation(res, operation === "set" ? "namespace, key, scope (global|workspace|user), and valueJson are required" : "namespace, key, and scope (global|workspace|user) are required");
160
+ return;
161
+ }
162
+ const target = resolveTargetWorkspaceId({ workspaceId, input: { bodyWorkspaceId: body.workspaceId, scope } });
163
+ if (!target.ok) {
164
+ validation(res, target.error);
165
+ return;
166
+ }
167
+ const principalId = body.principalId ? String(body.principalId) : undefined;
168
+ const permission = settingsWritePermission({ permissions, scope, callerPrincipalId, targetPrincipalId: principalId });
169
+ if (!await check(res, callerPrincipalId, permission, "setting-value"))
170
+ return;
171
+ const input = { namespace, key, scope, workspaceId: target.workspaceId, principalId, callerPrincipalId, authWorkspaceId: workspaceId };
172
+ const result = operation === "set" ? await service.set({ ...input, value: body.valueJson }) : await service.clear(input);
173
+ res.json({ key: `${namespace}.${key}`, scope, value: "value" in result ? result.value : null, revisionSeq: result.revisionSeq });
174
+ };
175
+ app.put(routes.value, wrap(mutate("set"), VALUE_ERRORS));
176
+ app.delete(routes.value, wrap(mutate("clear"), VALUE_ERRORS.filter((m) => m.code !== "VALUE_VALIDATION_FAILED")));
177
+ app.post(routes.reset, wrap(async (req, res, callerPrincipalId) => {
178
+ const body = bodyOf(req);
179
+ const namespace = String(body.namespace ?? "");
180
+ const scope = body.scope;
181
+ if (!namespace || !scopes.includes(scope)) {
182
+ validation(res, "namespace and scope (global|workspace|user) are required");
183
+ return;
184
+ }
185
+ const target = resolveTargetWorkspaceId({ workspaceId, input: { bodyWorkspaceId: body.workspaceId, scope } });
186
+ if (!target.ok) {
187
+ validation(res, target.error);
188
+ return;
189
+ }
190
+ if (!await check(res, callerPrincipalId, permissions.reset[scope], "setting-namespace"))
191
+ return;
192
+ const result = await service.reset({ namespace, scope, workspaceId: target.workspaceId, callerPrincipalId, authWorkspaceId: workspaceId });
193
+ res.json({ namespace, ...result });
194
+ }, RESET_ERRORS));
195
+ app.get(routes.events, wrap(async (req, res, principalId) => {
196
+ if (!await check(res, principalId, permissions.read, "setting-value"))
197
+ return;
198
+ const close = await openSettingsChangeStream({ request: req, response: res, workspaceId, principalId,
199
+ changeFeed: required.changeFeed, authorize: required.authorize, permission: permissions.read, scheduler: required.scheduler,
200
+ onClose: ({ close }) => { streams.delete(close); }, isDisposed: () => disposed }, optional);
201
+ if (close) {
202
+ if (disposed)
203
+ close();
204
+ else if (!res.destroyed && !res.writableEnded)
205
+ streams.add(close);
206
+ }
207
+ }));
208
+ return { dispose: () => { disposed = true; for (const close of streams)
209
+ close(); streams.clear(); } };
210
+ }
211
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,5 @@
1
+ /** @deprecated For one compatibility release: use @jini-ai/core/settings/express.
2
+ * Re-export the owner so route lifecycle and permission handling cannot drift.
3
+ */
4
+ export * from "@jini-ai/core/settings/express";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ /** @deprecated For one compatibility release: use @jini-ai/core/settings/express.
2
+ * Re-export the owner so route lifecycle and permission handling cannot drift.
3
+ */
4
+ export * from "@jini-ai/core/settings/express";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,68 @@
1
+ import type { Response } from "express";
2
+ import type { SettingScope } from "../index.js";
3
+ import type { Authorize, SettingsPermissions } from "./contracts.js";
4
+ export type TargetWorkspaceResolution = {
5
+ ok: true;
6
+ workspaceId: string | undefined;
7
+ } | {
8
+ ok: false;
9
+ error: string;
10
+ };
11
+ /** Pin writes to the authorized workspace, rejecting a conflicting body target.
12
+ * Global scope has no target workspace. Pure; O(1) time/space for string ids.
13
+ * @example resolveTargetWorkspaceId({ workspaceId, input: { scope: "workspace", bodyWorkspaceId } });
14
+ */
15
+ export declare function resolveTargetWorkspaceId(required: {
16
+ workspaceId: string;
17
+ input: {
18
+ bodyWorkspaceId: unknown;
19
+ scope: SettingScope;
20
+ };
21
+ }): TargetWorkspaceResolution;
22
+ export type UserLayerReadTarget = {
23
+ allowed: true;
24
+ principalId: string | undefined;
25
+ } | {
26
+ allowed: false;
27
+ reason: string;
28
+ };
29
+ /** Require the injected cross-principal grant only when a read targets another user.
30
+ * Returns the denial reason without reading values; one authorization effect at most.
31
+ * @complexity O(1) excluding the authorizer.
32
+ * @example resolveUserLayerReadTarget({ deps: { workspaceId, authorize, permission }, input });
33
+ */
34
+ export declare function resolveUserLayerReadTarget(required: {
35
+ deps: {
36
+ workspaceId: string;
37
+ authorize: Authorize;
38
+ permission: string;
39
+ };
40
+ input: {
41
+ requestedPrincipalId: string | undefined;
42
+ callerPrincipalId: string;
43
+ };
44
+ }): Promise<UserLayerReadTarget>;
45
+ export interface SettingsErrorMapping {
46
+ readonly matches: (error: unknown) => boolean;
47
+ readonly status: number;
48
+ readonly code: string;
49
+ }
50
+ /** Write the first matching status/code, or a fixed 500 that does not disclose unknown errors.
51
+ * @complexity O(mapping count) time, O(1) space; response write effect.
52
+ * @example respondToSettingsError({ response, error, mappings });
53
+ */
54
+ export declare function respondToSettingsError(required: {
55
+ response: Pick<Response, "status" | "json">;
56
+ error: unknown;
57
+ mappings: readonly SettingsErrorMapping[];
58
+ }): void;
59
+ /** Select caller policy for the CMS scope and self/other-user split; pure, O(1).
60
+ * @example settingsWritePermission({ permissions, scope: "user", callerPrincipalId, targetPrincipalId });
61
+ */
62
+ export declare function settingsWritePermission(required: {
63
+ permissions: SettingsPermissions;
64
+ scope: SettingScope;
65
+ callerPrincipalId: string;
66
+ targetPrincipalId: string | undefined;
67
+ }): string;
68
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1,54 @@
1
+ /** Pin writes to the authorized workspace, rejecting a conflicting body target.
2
+ * Global scope has no target workspace. Pure; O(1) time/space for string ids.
3
+ * @example resolveTargetWorkspaceId({ workspaceId, input: { scope: "workspace", bodyWorkspaceId } });
4
+ */
5
+ export function resolveTargetWorkspaceId(required) {
6
+ const { workspaceId, input } = required;
7
+ const named = input.bodyWorkspaceId === undefined || input.bodyWorkspaceId === null || input.bodyWorkspaceId === ""
8
+ ? undefined : String(input.bodyWorkspaceId);
9
+ if (named !== undefined && named !== workspaceId) {
10
+ return { ok: false, error: `workspaceId '${named}' does not match this route's workspace; a settings write cannot target another workspace` };
11
+ }
12
+ return { ok: true, workspaceId: input.scope === "global" ? undefined : workspaceId };
13
+ }
14
+ /** Require the injected cross-principal grant only when a read targets another user.
15
+ * Returns the denial reason without reading values; one authorization effect at most.
16
+ * @complexity O(1) excluding the authorizer.
17
+ * @example resolveUserLayerReadTarget({ deps: { workspaceId, authorize, permission }, input });
18
+ */
19
+ export async function resolveUserLayerReadTarget(required) {
20
+ const { deps, input } = required;
21
+ if (!input.requestedPrincipalId || input.requestedPrincipalId === input.callerPrincipalId) {
22
+ return { allowed: true, principalId: input.requestedPrincipalId };
23
+ }
24
+ const result = await deps.authorize({ principalId: input.callerPrincipalId, permission: deps.permission,
25
+ workspaceId: deps.workspaceId, entityType: "setting-value" });
26
+ return result.allowed ? { allowed: true, principalId: input.requestedPrincipalId } : { allowed: false, reason: result.reason };
27
+ }
28
+ /** Write the first matching status/code, or a fixed 500 that does not disclose unknown errors.
29
+ * @complexity O(mapping count) time, O(1) space; response write effect.
30
+ * @example respondToSettingsError({ response, error, mappings });
31
+ */
32
+ export function respondToSettingsError(required) {
33
+ const { response, error, mappings } = required;
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ for (const mapping of mappings) {
36
+ if (mapping.matches(error)) {
37
+ response.status(mapping.status).json({ error: message, code: mapping.code });
38
+ return;
39
+ }
40
+ }
41
+ response.status(500).json({ error: "internal error", code: "INTERNAL_ERROR" });
42
+ }
43
+ /** Select caller policy for the CMS scope and self/other-user split; pure, O(1).
44
+ * @example settingsWritePermission({ permissions, scope: "user", callerPrincipalId, targetPrincipalId });
45
+ */
46
+ export function settingsWritePermission(required) {
47
+ const { permissions, scope, callerPrincipalId, targetPrincipalId } = required;
48
+ if (scope === "global")
49
+ return permissions.writeGlobal;
50
+ if (scope === "workspace")
51
+ return permissions.writeWorkspace;
52
+ return targetPrincipalId == null || targetPrincipalId === callerPrincipalId ? permissions.writeUserSelf : permissions.writeUserOther;
53
+ }
54
+ //# sourceMappingURL=shared.js.map