@cosmicdrift/kumiko-bundled-features 0.55.0 → 0.55.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.55.0",
3
+ "version": "0.55.1",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -175,6 +175,20 @@ const integrationFeature = defineFeature("integration", (r) => {
175
175
  default: "initial",
176
176
  write: access.roles("Admin"),
177
177
  }),
178
+ // Settings-Hub system-scope proxies for the derived Stripe screen: a
179
+ // privileged boolean (billing-live = machine OR human-SystemAdmin) and
180
+ // an encrypted system secret (api-key). Both are surfaced to a human
181
+ // SystemAdmin by build-config-feature-schema and must be SET-able by them.
182
+ billingLive: createSystemConfig("boolean", {
183
+ default: false,
184
+ write: access.privileged,
185
+ read: access.admin,
186
+ }),
187
+ systemSecret: createSystemConfig("text", {
188
+ write: access.systemAdmin,
189
+ read: access.systemAdmin,
190
+ encrypted: true,
191
+ }),
178
192
  },
179
193
  });
180
194
  });
@@ -330,6 +344,42 @@ describe("scenario 1: system-scoped service URL", () => {
330
344
  });
331
345
  });
332
346
 
347
+ // Settings-Hub round-trip: a human SystemAdmin saves the system-scope keys
348
+ // the derived configEdit screen surfaces (privileged boolean + encrypted
349
+ // secret), then reads them back. Regression for the checkWriteAccess bug
350
+ // that rejected the operator on a `privileged` key with config.errors.systemOnly.
351
+ describe("Settings-Hub system-scope write by a human SystemAdmin", () => {
352
+ const sysAccessor = () =>
353
+ createConfigAccessor(stack.registry, resolver, systemAdmin.tenantId, systemAdmin.id, db);
354
+
355
+ test("saves a privileged boolean (billing-live) and reads it back", async () => {
356
+ await stack.http.writeOk(
357
+ ConfigHandlers.set,
358
+ { key: "integration:config:billing-live", value: true, scope: "system" },
359
+ systemAdmin,
360
+ );
361
+ expect(await sysAccessor()("integration:config:billing-live")).toBe(true);
362
+ });
363
+
364
+ test("saves an encrypted system secret (api-key) and reads it back decrypted", async () => {
365
+ await stack.http.writeOk(
366
+ ConfigHandlers.set,
367
+ { key: "integration:config:system-secret", value: "sk_live_roundtrip", scope: "system" },
368
+ systemAdmin,
369
+ );
370
+ expect(await sysAccessor()("integration:config:system-secret")).toBe("sk_live_roundtrip");
371
+ });
372
+
373
+ test("a plain tenant Admin is denied the privileged key (not via system-only)", async () => {
374
+ const error = await stack.http.writeErr(
375
+ ConfigHandlers.set,
376
+ { key: "integration:config:billing-live", value: false, scope: "system" },
377
+ tenantAdmin,
378
+ );
379
+ expectErrorIncludes(error, "access_denied");
380
+ });
381
+ });
382
+
333
383
  // --- Scenario 2: Mail Server — system default + tenant override ---
334
384
 
335
385
  describe("scenario 2: tenant-scoped mail server with system fallback", () => {
@@ -105,6 +105,40 @@ describe("prepareConfigWrite", () => {
105
105
  expect(result.failure.error.i18nKey).toBe("config.errors.systemOnly");
106
106
  });
107
107
 
108
+ test("privileged key (system + SystemAdmin) is writable by a human SystemAdmin", () => {
109
+ // The derived configEdit screen surfaces a `access.privileged`
110
+ // (`["system","SystemAdmin"]`) key to a human SystemAdmin (e.g. Stripe
111
+ // billing-live). The write must succeed — "system in the write-set"
112
+ // means machine-OR-operator, not machine-only.
113
+ const privilegedKey = createSystemConfig("boolean", { write: access.privileged });
114
+ const result = prepareConfigWrite({
115
+ registry: registryStub({ "ns:config:billing-live": privilegedKey }),
116
+ user: userStub(["SystemAdmin"], SYSTEM_TENANT_ID, "sysadmin-1"),
117
+ key: "ns:config:billing-live",
118
+ scope: ConfigScopes.system,
119
+ });
120
+ expect(result.ok).toBe(true);
121
+ if (!result.ok) throw new Error("unreachable");
122
+ expect(result.keyDef).toBe(privilegedKey);
123
+ });
124
+
125
+ test("privileged key stays blocked for a non-SystemAdmin human (generic denied, not system-only)", () => {
126
+ // Security: the human half of `privileged` is SystemAdmin only — a plain
127
+ // Admin must NOT inherit it. And the error is the generic access-denied,
128
+ // not systemOnly (the key has a human writer, it just isn't this user).
129
+ const privilegedKey = createSystemConfig("boolean", { write: access.privileged });
130
+ const result = prepareConfigWrite({
131
+ registry: registryStub({ "ns:config:billing-live": privilegedKey }),
132
+ user: userStub(["Admin"]),
133
+ key: "ns:config:billing-live",
134
+ scope: ConfigScopes.system,
135
+ });
136
+ expect(result.ok).toBe(false);
137
+ if (result.ok) throw new Error("unreachable");
138
+ expect(result.failure.error.code).toBe("access_denied");
139
+ expect(result.failure.error.i18nKey).not.toBe("config.errors.systemOnly");
140
+ });
141
+
108
142
  test("ok-path falls back to the key's declared scope when no scope is passed", () => {
109
143
  const result = prepareConfigWrite({
110
144
  registry: registryStub({ "ns:config:foo": TENANT_KEY_DEF }),
@@ -15,11 +15,17 @@ export const defaultTranslations: TranslationsByLocale = {
15
15
  "config.settings.system": "Plattform",
16
16
  "config.settings.tenant": "Organisation",
17
17
  "config.settings.user": "Persönlich",
18
+ "config.errors.systemOnly": "Dieser Wert kann nur vom System gesetzt werden.",
19
+ "config.errors.invalidScope": "Diese Ebene ist für diesen Schlüssel nicht zulässig.",
20
+ "config.errors.unknownKey": "Unbekannter Konfigurationsschlüssel.",
18
21
  },
19
22
  en: {
20
23
  "config.settings.title": "Settings",
21
24
  "config.settings.system": "Platform",
22
25
  "config.settings.tenant": "Organization",
23
26
  "config.settings.user": "Personal",
27
+ "config.errors.systemOnly": "This value can only be set by the system.",
28
+ "config.errors.invalidScope": "This scope is not allowed for this key.",
29
+ "config.errors.unknownKey": "Unknown configuration key.",
24
30
  },
25
31
  };
@@ -117,26 +117,28 @@ export function checkWriteAccess(
117
117
  keyDef: ConfigKeyDefinition,
118
118
  userRoles: readonly string[],
119
119
  ): KumikoError | null {
120
- if (keyDef.access.write.includes(SYSTEM_ROLE)) {
121
- // Pre-ES the system-only block was absolute out-of-band writes went
122
- // through resolver.set, bypassing the whole access layer. Post-ES
123
- // every write flows through this handler + executor, so the escape
124
- // hatch becomes explicit: SYSTEM_ROLE (jobs / seeds / framework-
125
- // internal work) may write; everyone else is rejected.
126
- if (userRoles.includes(SYSTEM_ROLE)) return null;
120
+ // Direct grant first: matches SYSTEM_ROLE for the machine actor (jobs /
121
+ // seeds / framework-internal work) AND a human role (e.g. SystemAdmin)
122
+ // that the write-set lists. A privileged key (`["system", "SystemAdmin"]`)
123
+ // is therefore writable by BOTH the escape hatch and the operator —
124
+ // which is what the derived configEdit screen (build-config-feature-schema
125
+ // scopedKeysAt) already assumes when it surfaces the key to a human.
126
+ if (hasConfigAccess(keyDef.access.write, userRoles)) return null;
127
+ // No direct grant. A key whose only writer is SYSTEM_ROLE is machine-only
128
+ // (the `access.system` preset): no human may write it, distinct error so
129
+ // the UI can explain "operator/job-only".
130
+ const humanWriters = keyDef.access.write.filter((role) => role !== SYSTEM_ROLE);
131
+ if (humanWriters.length === 0) {
127
132
  return new AccessDeniedError({
128
133
  message: "config key is system-only",
129
134
  i18nKey: "config.errors.systemOnly",
130
135
  details: { reason: ConfigErrors.systemOnly },
131
136
  });
132
137
  }
133
- if (!hasConfigAccess(keyDef.access.write, userRoles)) {
134
- return new AccessDeniedError({
135
- message: "config write access denied",
136
- details: { requiredRoles: keyDef.access.write },
137
- });
138
- }
139
- return null;
138
+ return new AccessDeniedError({
139
+ message: "config write access denied",
140
+ details: { requiredRoles: keyDef.access.write },
141
+ });
140
142
  }
141
143
 
142
144
  export function validateScope(