@camstack/types 1.2.451 → 1.2.453

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 (47) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/addon/base-addon.d.ts +27 -0
  3. package/dist/addon.js +8 -3
  4. package/dist/addon.mjs +8 -3
  5. package/dist/automation/condition-eval.d.ts +20 -5
  6. package/dist/automation/detection-subject.d.ts +154 -0
  7. package/dist/automation/detection-trigger.d.ts +16 -6
  8. package/dist/automation/index.d.ts +10 -4
  9. package/dist/automation/legacy-recipe.d.ts +130 -0
  10. package/dist/automation/normalize-units.d.ts +4 -3
  11. package/dist/automation/recipe.d.ts +286 -142
  12. package/dist/automation/stationary-trigger.d.ts +4 -4
  13. package/dist/automation/step-runner.d.ts +3 -1
  14. package/dist/automation/trace.d.ts +51 -6
  15. package/dist/automation/when.d.ts +295 -0
  16. package/dist/capabilities/actions.cap.d.ts +10 -0
  17. package/dist/capabilities/addon-settings.cap.d.ts +18 -0
  18. package/dist/capabilities/core-blocks.cap.d.ts +2546 -51
  19. package/dist/capabilities/device-discovery.cap.d.ts +2 -0
  20. package/dist/capabilities/device-manager.cap.d.ts +16 -1
  21. package/dist/capabilities/device-provider.cap.d.ts +165 -0
  22. package/dist/capabilities/index.d.ts +3 -3
  23. package/dist/capabilities/integrations.cap.d.ts +1 -0
  24. package/dist/capabilities/keychain.cap.d.ts +12 -0
  25. package/dist/capabilities/local-network.cap.d.ts +198 -0
  26. package/dist/capabilities/storage-admin.cap.d.ts +155 -0
  27. package/dist/capabilities/terminal-session.cap.d.ts +7 -0
  28. package/dist/client.js +8 -1
  29. package/dist/client.mjs +8 -1
  30. package/dist/device/base-device-provider.d.ts +17 -0
  31. package/dist/generated/addon-api.d.ts +84 -0
  32. package/dist/generated/collection-array-methods.d.ts +1 -1
  33. package/dist/generated/method-access-map.d.ts +1 -1
  34. package/dist/generated/system-proxy.d.ts +3 -3
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +1969 -706
  37. package/dist/index.mjs +1934 -703
  38. package/dist/interfaces/addon.d.ts +16 -0
  39. package/dist/interfaces/config-ui-secrets.d.ts +5 -3
  40. package/dist/interfaces/config-ui.d.ts +34 -1
  41. package/dist/{sleep-BSPhInRm.js → sleep-CewR0zRz.js} +125 -6
  42. package/dist/{sleep-63gM_1JR.mjs → sleep-CeyhrcOk.mjs} +125 -6
  43. package/dist/vendor-login-proxy/cookie-jar.d.ts +30 -0
  44. package/dist/vendor-login-proxy/index.d.ts +1 -0
  45. package/dist/vendor-login-proxy/login-proxy.d.ts +6 -0
  46. package/dist/vendor-login-proxy/url-rewrite.d.ts +9 -2
  47. package/package.json +1 -1
@@ -895,6 +895,22 @@ export interface ICamstackAddon {
895
895
  * stays the one bare key. See `addon-settings.cap.ts`.
896
896
  */
897
897
  getIntegrationSettings?(nodeId?: string): Promise<ConfigUISchemaWithValues | null>;
898
+ /**
899
+ * Stale-data audit: the global-settings keys this addon still declares,
900
+ * answered in its own process. See `BaseAddon.getDeclaredSettingsKeys`.
901
+ */
902
+ getDeclaredSettingsKeys?(): Promise<{
903
+ readonly keys: readonly string[];
904
+ }>;
905
+ /** Stale-data audit: remove stored keys the addon no longer declares. */
906
+ unsetStoredSettingsKeys?(keys: readonly string[]): Promise<{
907
+ readonly removed: readonly string[];
908
+ readonly refused: ReadonlyArray<{
909
+ readonly key: string;
910
+ readonly reason: string;
911
+ }>;
912
+ readonly secretsCleared: number;
913
+ }>;
898
914
  /** Level 2 — per-device settings (schema + values). Appears in
899
915
  * Device Overrides. */
900
916
  getDeviceSettings?(deviceId: number): Promise<ConfigUISchemaWithValues>;
@@ -45,14 +45,16 @@ export declare function isKeychainStoredField(field: ConfigField): boolean;
45
45
  * inside `group` / `sub-tabs` containers.
46
46
  */
47
47
  export declare function collectSecretConfigKeys(schema: unknown): ReadonlySet<string>;
48
+ /** Drop fields marked `omitFromSettings` before an integration settings write. */
49
+ export declare function omitTransientConfig(schema: unknown, config: Record<string, unknown>): Record<string, unknown>;
48
50
  /**
49
- * Every top-level config key whose value the KEYCHAIN stores (D787) —
51
+ * Every config key whose value the KEYCHAIN stores (D787) —
50
52
  * {@link collectSecretConfigKeys} minus `storage: 'inline'` and minus JSON
51
53
  * blobs (see {@link isKeychainStoredField}). What a form sends as
52
54
  * `secretKeys` so the owner's door knows which values to keep out of the blob.
55
+ * A secret inside an `editable-array` row is named by its id-addressed path
56
+ * (`boards[id].encryptionKey`).
53
57
  */
54
- /** Drop fields marked `omitFromSettings` before an integration settings write. */
55
- export declare function omitTransientConfig(schema: unknown, config: Record<string, unknown>): Record<string, unknown>;
56
58
  export declare function collectKeychainStoredKeys(schema: unknown): ReadonlySet<string>;
57
59
  /**
58
60
  * True when the value is a readable schema declaring at least one field of any
@@ -86,7 +86,7 @@ export interface ConfigSection {
86
86
  readonly immediate?: boolean;
87
87
  readonly fields: readonly ConfigField[];
88
88
  }
89
- export type ConfigField = ConfigTextField | ConfigNumberField | ConfigBooleanField | ConfigSelectField | ConfigMultiSelectField | ConfigTimezoneField | ConfigDateTimeField | ConfigColorField | ConfigIconField | ConfigPasswordField | ConfigTextAreaField | ConfigSliderField | ConfigTagsField | ConfigScheduleField | ConfigGroupField | ConfigSubTabsField | ConfigSeparatorField | ConfigInfoField | ConfigQrCodeField | ConfigObjectArrayField | ConfigEditableArrayField | ConfigModelSelectorField | ConfigStorageLocationField | ConfigProbeField | ConfigButtonField | ConfigNodeSelectField | ConfigNodeMultiSelectField | ConfigDeviceSelectField | ConfigDeviceMultiSelectField | ConfigNotificationTargetSelectField | ConfigNotificationTargetMultiSelectField | ConfigWidgetField | ConfigAddonActionSelectField | ConfigAddonActionButtonField | ConfigDeviceActionButtonField | ConfigAccountSignInField | ConfigSecretPresenceField;
89
+ export type ConfigField = ConfigTextField | ConfigNumberField | ConfigBooleanField | ConfigSelectField | ConfigMultiSelectField | ConfigTimezoneField | ConfigDateTimeField | ConfigColorField | ConfigIconField | ConfigPasswordField | ConfigTextAreaField | ConfigSliderField | ConfigTagsField | ConfigScheduleField | ConfigGroupField | ConfigSubTabsField | ConfigSeparatorField | ConfigInfoField | ConfigQrCodeField | ConfigObjectArrayField | ConfigEditableArrayField | ConfigModelSelectorField | ConfigStorageLocationField | ConfigProbeField | ConfigButtonField | ConfigNodeSelectField | ConfigNodeMultiSelectField | ConfigDeviceSelectField | ConfigDeviceMultiSelectField | ConfigNotificationTargetSelectField | ConfigNotificationTargetMultiSelectField | ConfigBrokerSelectField | ConfigWidgetField | ConfigAddonActionSelectField | ConfigAddonActionButtonField | ConfigDeviceActionButtonField | ConfigAccountSignInField | ConfigSecretPresenceField;
90
90
  /**
91
91
  * Per-MODEL applicability of a pipeline-step setting (D214).
92
92
  *
@@ -385,6 +385,27 @@ export interface ConfigSelectField extends ConfigFieldBase {
385
385
  */
386
386
  optionsFrom?: string;
387
387
  dependsOn?: Record<string, unknown>;
388
+ /**
389
+ * Options fetched from the provider of a notification kind once what it needs
390
+ * has been entered. See {@link ConfigDiscoverOptions}. Free text stays
391
+ * possible when the fetch fails or answers nothing.
392
+ */
393
+ discoverOptions?: ConfigDiscoverOptions;
394
+ }
395
+ /**
396
+ * A select whose options are the candidates a notification kind's
397
+ * `discoverTargets` answers for the form's CURRENT values.
398
+ *
399
+ * Nothing is fetched until every field in `dependsOn` has a value (a broker
400
+ * picked, a token typed), so a credential-gated kind is never asked without its
401
+ * credentials. A candidate becomes an option: value = `candidate.config[<this
402
+ * field's key>]`, label = `candidate.suggestedName`.
403
+ */
404
+ export interface ConfigDiscoverOptions {
405
+ /** The notification kind whose provider answers the discovery. */
406
+ readonly kind: string;
407
+ /** Form keys that must all be non-empty before the first fetch; sent as `config`. */
408
+ readonly dependsOn: readonly string[];
388
409
  }
389
410
  export interface ConfigMultiSelectField extends ConfigFieldBase {
390
411
  type: 'multiselect';
@@ -520,6 +541,16 @@ export interface ConfigDeviceMultiSelectField extends ConfigFieldBase {
520
541
  export interface ConfigNotificationTargetSelectField extends ConfigFieldBase {
521
542
  type: 'notification-target-select';
522
543
  }
544
+ /**
545
+ * Pick an existing broker of one kind, or create one in place. Stored value is
546
+ * the broker id string. The host mounts the picker (it owns the sign-in
547
+ * dialogs); without one the field lists the brokers without "Create new".
548
+ */
549
+ export interface ConfigBrokerSelectField extends ConfigFieldBase {
550
+ type: 'broker-select';
551
+ /** The broker kind to list and to create. */
552
+ brokerKind: string;
553
+ }
523
554
  /**
524
555
  * Pick MULTIPLE `notification-output` targets. Stored value is a `string[]`
525
556
  * of target ids.
@@ -1136,6 +1167,8 @@ export type ConfigFieldWithValue = (ConfigTextField & {
1136
1167
  readonly value: unknown;
1137
1168
  }) | (ConfigNotificationTargetMultiSelectField & {
1138
1169
  readonly value: unknown;
1170
+ }) | (ConfigBrokerSelectField & {
1171
+ readonly value: unknown;
1139
1172
  }) | ConfigWidgetField | (ConfigAddonActionSelectField & {
1140
1173
  readonly value: unknown;
1141
1174
  }) | ConfigAddonActionButtonField | ConfigDeviceActionButtonField | ConfigAccountSignInField | ConfigSecretPresenceField;
@@ -150,12 +150,6 @@ function collectSecretConfigKeys(schema) {
150
150
  for (const section of sectionsOf(schema)) for (const field of fieldsOf(section)) walkField(field, keys);
151
151
  return keys;
152
152
  }
153
- /**
154
- * Every top-level config key whose value the KEYCHAIN stores (D787) —
155
- * {@link collectSecretConfigKeys} minus `storage: 'inline'` and minus JSON
156
- * blobs (see {@link isKeychainStoredField}). What a form sends as
157
- * `secretKeys` so the owner's door knows which values to keep out of the blob.
158
- */
159
153
  /** Drop fields marked `omitFromSettings` before an integration settings write. */
160
154
  function omitTransientConfig(schema, config) {
161
155
  const omit = /* @__PURE__ */ new Set();
@@ -180,6 +174,14 @@ function collectOmitted(field, out) {
180
174
  for (const tab of tabs) for (const child of fieldsOf(tab)) collectOmitted(child, out);
181
175
  }
182
176
  }
177
+ /**
178
+ * Every config key whose value the KEYCHAIN stores (D787) —
179
+ * {@link collectSecretConfigKeys} minus `storage: 'inline'` and minus JSON
180
+ * blobs (see {@link isKeychainStoredField}). What a form sends as
181
+ * `secretKeys` so the owner's door knows which values to keep out of the blob.
182
+ * A secret inside an `editable-array` row is named by its id-addressed path
183
+ * (`boards[id].encryptionKey`).
184
+ */
183
185
  function collectKeychainStoredKeys(schema) {
184
186
  const keys = /* @__PURE__ */ new Set();
185
187
  for (const section of sectionsOf(schema)) for (const field of fieldsOf(section)) walkKeychainField(field, keys);
@@ -192,6 +194,17 @@ function walkKeychainField(field, out) {
192
194
  const isSecret = type === "password" || field["secret"] === true;
193
195
  const isJsonBlob = type === "textarea" && field["isJson"] === true;
194
196
  if (isSecret && !isJsonBlob && field["storage"] !== "inline" && typeof key === "string" && key.length > 0) out.add(key);
197
+ if (type === "editable-array" && typeof key === "string" && key.length > 0) {
198
+ const idKey = typeof field["itemIdKey"] === "string" ? field["itemIdKey"] : "id";
199
+ const itemFields = field["itemFields"];
200
+ if (!Array.isArray(itemFields)) return;
201
+ for (const item of itemFields) {
202
+ const members = /* @__PURE__ */ new Set();
203
+ walkKeychainField(item, members);
204
+ for (const member of members) out.add(`${key}[${idKey}].${member}`);
205
+ }
206
+ return;
207
+ }
195
208
  if (type === "group") {
196
209
  for (const child of fieldsOf(field)) walkKeychainField(child, out);
197
210
  return;
@@ -2414,6 +2427,7 @@ var BaseAddon = class {
2414
2427
  * a warning and falls back rather than crashing boot.
2415
2428
  */
2416
2429
  state(key, schema, fallback) {
2430
+ this._declaredStateKeys.add(key);
2417
2431
  return createDurableState({
2418
2432
  key,
2419
2433
  schema,
@@ -2429,6 +2443,96 @@ var BaseAddon = class {
2429
2443
  } })
2430
2444
  });
2431
2445
  }
2446
+ /** Durable-state keys this process registered through {@link state}. */
2447
+ _declaredStateKeys = /* @__PURE__ */ new Set();
2448
+ /**
2449
+ * Stale-data audit: every global-settings key this addon still declares —
2450
+ * constructor defaults, settings-schema fields (layout containers included),
2451
+ * durable-state keys and the root of each `secretStatePaths` entry. Answered
2452
+ * here, in the addon's own process, so the hub keeps no copy of it.
2453
+ */
2454
+ async getDeclaredSettingsKeys() {
2455
+ return { keys: [...this.declaredKeySet()].toSorted() };
2456
+ }
2457
+ declaredKeySet() {
2458
+ const keys = new Set(Object.keys(this.defaults));
2459
+ const schema = this.globalSettingsSchema();
2460
+ if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
2461
+ for (const key of this._declaredStateKeys) keys.add(key);
2462
+ for (const path of this.secretStatePaths) {
2463
+ const root = path.split(/[.[]/, 1)[0];
2464
+ if (root) keys.add(root);
2465
+ }
2466
+ return keys;
2467
+ }
2468
+ /**
2469
+ * Stale-data audit: remove stored global-settings keys the addon no longer
2470
+ * declares, and clear the keychain values their markers held — except a value
2471
+ * a surviving key still points at. A declared or absent key is refused.
2472
+ */
2473
+ async unsetStoredSettingsKeys(keys) {
2474
+ const settings = this._ctx?.settings;
2475
+ const raw = this.rawSettings();
2476
+ if (!settings || !raw) return {
2477
+ removed: [],
2478
+ refused: keys.map((key) => ({
2479
+ key,
2480
+ reason: "no settings store"
2481
+ })),
2482
+ secretsCleared: 0
2483
+ };
2484
+ let stored;
2485
+ try {
2486
+ stored = await raw.readAddonStore();
2487
+ } catch (error) {
2488
+ const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
2489
+ return {
2490
+ removed: [],
2491
+ refused: keys.map((key) => ({
2492
+ key,
2493
+ reason
2494
+ })),
2495
+ secretsCleared: 0
2496
+ };
2497
+ }
2498
+ const declared = this.declaredKeySet();
2499
+ const removed = [];
2500
+ const refused = [];
2501
+ for (const key of keys) {
2502
+ const base = key.split("@", 1)[0] ?? key;
2503
+ if (declared.has(base)) refused.push({
2504
+ key,
2505
+ reason: "still declared by the addon"
2506
+ });
2507
+ else if (!(key in stored)) refused.push({
2508
+ key,
2509
+ reason: "not stored"
2510
+ });
2511
+ else removed.push(key);
2512
+ }
2513
+ if (removed.length === 0) return {
2514
+ removed,
2515
+ refused,
2516
+ secretsCleared: 0
2517
+ };
2518
+ const kept = /* @__PURE__ */ new Set();
2519
+ const held = /* @__PURE__ */ new Set();
2520
+ for (const [key, value] of Object.entries(stored)) {
2521
+ const target = removed.includes(key) ? held : kept;
2522
+ for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
2523
+ }
2524
+ const refsToClear = [...held].filter((ref) => !kept.has(ref));
2525
+ const patch = {};
2526
+ for (const key of removed) patch[key] = void 0;
2527
+ await settings.writeAddonStore(patch);
2528
+ const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
2529
+ await this.resolveConfig();
2530
+ return {
2531
+ removed,
2532
+ refused,
2533
+ secretsCleared
2534
+ };
2535
+ }
2432
2536
  /** Per-device variant of {@link state}, backed by the per-device store. */
2433
2537
  deviceState(deviceId, key, schema, fallback) {
2434
2538
  return createDurableState({
@@ -2699,6 +2803,21 @@ function dropPerNodeFields(fields) {
2699
2803
  }
2700
2804
  return kept;
2701
2805
  }
2806
+ function collectFieldKeys(fields) {
2807
+ const collected = [];
2808
+ for (const field of fields) {
2809
+ if (field.type === "group") {
2810
+ collected.push(...collectFieldKeys(field.fields));
2811
+ continue;
2812
+ }
2813
+ if (field.type === "sub-tabs") {
2814
+ for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
2815
+ continue;
2816
+ }
2817
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
2818
+ }
2819
+ return collected;
2820
+ }
2702
2821
  function collectPerNodeFieldKeys(fields) {
2703
2822
  const collected = [];
2704
2823
  for (const field of fields) {
@@ -150,12 +150,6 @@ function collectSecretConfigKeys(schema) {
150
150
  for (const section of sectionsOf(schema)) for (const field of fieldsOf(section)) walkField(field, keys);
151
151
  return keys;
152
152
  }
153
- /**
154
- * Every top-level config key whose value the KEYCHAIN stores (D787) —
155
- * {@link collectSecretConfigKeys} minus `storage: 'inline'` and minus JSON
156
- * blobs (see {@link isKeychainStoredField}). What a form sends as
157
- * `secretKeys` so the owner's door knows which values to keep out of the blob.
158
- */
159
153
  /** Drop fields marked `omitFromSettings` before an integration settings write. */
160
154
  function omitTransientConfig(schema, config) {
161
155
  const omit = /* @__PURE__ */ new Set();
@@ -180,6 +174,14 @@ function collectOmitted(field, out) {
180
174
  for (const tab of tabs) for (const child of fieldsOf(tab)) collectOmitted(child, out);
181
175
  }
182
176
  }
177
+ /**
178
+ * Every config key whose value the KEYCHAIN stores (D787) —
179
+ * {@link collectSecretConfigKeys} minus `storage: 'inline'` and minus JSON
180
+ * blobs (see {@link isKeychainStoredField}). What a form sends as
181
+ * `secretKeys` so the owner's door knows which values to keep out of the blob.
182
+ * A secret inside an `editable-array` row is named by its id-addressed path
183
+ * (`boards[id].encryptionKey`).
184
+ */
183
185
  function collectKeychainStoredKeys(schema) {
184
186
  const keys = /* @__PURE__ */ new Set();
185
187
  for (const section of sectionsOf(schema)) for (const field of fieldsOf(section)) walkKeychainField(field, keys);
@@ -192,6 +194,17 @@ function walkKeychainField(field, out) {
192
194
  const isSecret = type === "password" || field["secret"] === true;
193
195
  const isJsonBlob = type === "textarea" && field["isJson"] === true;
194
196
  if (isSecret && !isJsonBlob && field["storage"] !== "inline" && typeof key === "string" && key.length > 0) out.add(key);
197
+ if (type === "editable-array" && typeof key === "string" && key.length > 0) {
198
+ const idKey = typeof field["itemIdKey"] === "string" ? field["itemIdKey"] : "id";
199
+ const itemFields = field["itemFields"];
200
+ if (!Array.isArray(itemFields)) return;
201
+ for (const item of itemFields) {
202
+ const members = /* @__PURE__ */ new Set();
203
+ walkKeychainField(item, members);
204
+ for (const member of members) out.add(`${key}[${idKey}].${member}`);
205
+ }
206
+ return;
207
+ }
195
208
  if (type === "group") {
196
209
  for (const child of fieldsOf(field)) walkKeychainField(child, out);
197
210
  return;
@@ -2414,6 +2427,7 @@ var BaseAddon = class {
2414
2427
  * a warning and falls back rather than crashing boot.
2415
2428
  */
2416
2429
  state(key, schema, fallback) {
2430
+ this._declaredStateKeys.add(key);
2417
2431
  return createDurableState({
2418
2432
  key,
2419
2433
  schema,
@@ -2429,6 +2443,96 @@ var BaseAddon = class {
2429
2443
  } })
2430
2444
  });
2431
2445
  }
2446
+ /** Durable-state keys this process registered through {@link state}. */
2447
+ _declaredStateKeys = /* @__PURE__ */ new Set();
2448
+ /**
2449
+ * Stale-data audit: every global-settings key this addon still declares —
2450
+ * constructor defaults, settings-schema fields (layout containers included),
2451
+ * durable-state keys and the root of each `secretStatePaths` entry. Answered
2452
+ * here, in the addon's own process, so the hub keeps no copy of it.
2453
+ */
2454
+ async getDeclaredSettingsKeys() {
2455
+ return { keys: [...this.declaredKeySet()].toSorted() };
2456
+ }
2457
+ declaredKeySet() {
2458
+ const keys = new Set(Object.keys(this.defaults));
2459
+ const schema = this.globalSettingsSchema();
2460
+ if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
2461
+ for (const key of this._declaredStateKeys) keys.add(key);
2462
+ for (const path of this.secretStatePaths) {
2463
+ const root = path.split(/[.[]/, 1)[0];
2464
+ if (root) keys.add(root);
2465
+ }
2466
+ return keys;
2467
+ }
2468
+ /**
2469
+ * Stale-data audit: remove stored global-settings keys the addon no longer
2470
+ * declares, and clear the keychain values their markers held — except a value
2471
+ * a surviving key still points at. A declared or absent key is refused.
2472
+ */
2473
+ async unsetStoredSettingsKeys(keys) {
2474
+ const settings = this._ctx?.settings;
2475
+ const raw = this.rawSettings();
2476
+ if (!settings || !raw) return {
2477
+ removed: [],
2478
+ refused: keys.map((key) => ({
2479
+ key,
2480
+ reason: "no settings store"
2481
+ })),
2482
+ secretsCleared: 0
2483
+ };
2484
+ let stored;
2485
+ try {
2486
+ stored = await raw.readAddonStore();
2487
+ } catch (error) {
2488
+ const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
2489
+ return {
2490
+ removed: [],
2491
+ refused: keys.map((key) => ({
2492
+ key,
2493
+ reason
2494
+ })),
2495
+ secretsCleared: 0
2496
+ };
2497
+ }
2498
+ const declared = this.declaredKeySet();
2499
+ const removed = [];
2500
+ const refused = [];
2501
+ for (const key of keys) {
2502
+ const base = key.split("@", 1)[0] ?? key;
2503
+ if (declared.has(base)) refused.push({
2504
+ key,
2505
+ reason: "still declared by the addon"
2506
+ });
2507
+ else if (!(key in stored)) refused.push({
2508
+ key,
2509
+ reason: "not stored"
2510
+ });
2511
+ else removed.push(key);
2512
+ }
2513
+ if (removed.length === 0) return {
2514
+ removed,
2515
+ refused,
2516
+ secretsCleared: 0
2517
+ };
2518
+ const kept = /* @__PURE__ */ new Set();
2519
+ const held = /* @__PURE__ */ new Set();
2520
+ for (const [key, value] of Object.entries(stored)) {
2521
+ const target = removed.includes(key) ? held : kept;
2522
+ for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
2523
+ }
2524
+ const refsToClear = [...held].filter((ref) => !kept.has(ref));
2525
+ const patch = {};
2526
+ for (const key of removed) patch[key] = void 0;
2527
+ await settings.writeAddonStore(patch);
2528
+ const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
2529
+ await this.resolveConfig();
2530
+ return {
2531
+ removed,
2532
+ refused,
2533
+ secretsCleared
2534
+ };
2535
+ }
2432
2536
  /** Per-device variant of {@link state}, backed by the per-device store. */
2433
2537
  deviceState(deviceId, key, schema, fallback) {
2434
2538
  return createDurableState({
@@ -2699,6 +2803,21 @@ function dropPerNodeFields(fields) {
2699
2803
  }
2700
2804
  return kept;
2701
2805
  }
2806
+ function collectFieldKeys(fields) {
2807
+ const collected = [];
2808
+ for (const field of fields) {
2809
+ if (field.type === "group") {
2810
+ collected.push(...collectFieldKeys(field.fields));
2811
+ continue;
2812
+ }
2813
+ if (field.type === "sub-tabs") {
2814
+ for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
2815
+ continue;
2816
+ }
2817
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
2818
+ }
2819
+ return collected;
2820
+ }
2702
2821
  function collectPerNodeFieldKeys(fields) {
2703
2822
  const collected = [];
2704
2823
  for (const field of fields) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Server-side cookie jar of ONE vendor sign-in attempt (D955).
3
+ *
4
+ * A vendor login (an identity provider's cookie/JS check page) refuses a flow that arrives
5
+ * without the cookies it set a step earlier. Relying on the browser to store them under the
6
+ * hub origin loses them whenever the browser refuses the rewritten cookie (a `Secure` cookie
7
+ * on a plain-http hub, a `__Host-` name with a narrowed Path, a `SameSite=None` without
8
+ * `Secure`). The proxy therefore keeps every vendor cookie itself and sends them upstream,
9
+ * whatever the browser did. Values never leave this object except as the upstream `Cookie`
10
+ * header; nothing here logs.
11
+ */
12
+ import { type LoginProxyScheme } from './url-rewrite.js';
13
+ export declare class VendorCookieJar {
14
+ #private;
15
+ /** Store (or delete, when expired) the cookies of one upstream response. */
16
+ store(setCookies: readonly string[], origin: {
17
+ readonly host: string;
18
+ readonly pathname: string;
19
+ }, nowMs: number): void;
20
+ /** The `Cookie` header value for a request to `host` + `pathname`, or null when none applies. */
21
+ headerFor(target: {
22
+ readonly host: string;
23
+ readonly pathname: string;
24
+ readonly scheme: LoginProxyScheme;
25
+ }, nowMs: number): string | null;
26
+ }
27
+ /** Cookie names carried by a `Cookie` header value. */
28
+ export declare function cookieHeaderNames(header: string): Set<string>;
29
+ /** `browser` cookies not shadowed by the jar's, then the jar's: the jar is the truth. */
30
+ export declare function mergeCookieHeaders(browser: string, jar: string | null): string;
@@ -1,3 +1,4 @@
1
1
  export { VendorLoginProxy, type LoginProxyAttemptSpec, type LoginProxyResponseView, type LoginProxyVerdict, type VendorLoginProxyOptions, } from './login-proxy.js';
2
2
  export { rewriteBody, rewriteLocation, rewriteReferer, rewriteFormBody, rewriteSetCookie, rewriteSetCookies, type LoginProxyRoute, type LoginProxyScheme, type LoginProxyUpstream, } from './url-rewrite.js';
3
3
  export { VENDOR_LOGIN_PROXY_PREFIX } from './prefix.js';
4
+ export { VendorCookieJar } from './cookie-jar.js';
@@ -28,6 +28,12 @@ export interface LoginProxyAttemptSpec<T> {
28
28
  readonly terminal: (view: LoginProxyResponseView) => LoginProxyVerdict<T> | Promise<LoginProxyVerdict<T>>;
29
29
  /** Finish the sign-in with the payload; the outcome picks the result page the browser lands on. */
30
30
  readonly onDone: (payload: T) => Promise<OauthLandingOutcome>;
31
+ /**
32
+ * Keep the vendor's cookies on the server for this attempt and send them upstream, whatever
33
+ * the browser stored. For a login that refuses a flow arriving without the cookies it set
34
+ * earlier (a browser-requirements check page). Default off.
35
+ */
36
+ readonly serverCookieJar?: boolean;
31
37
  }
32
38
  export interface VendorLoginProxyOptions {
33
39
  readonly addonId: string;
@@ -57,15 +57,22 @@ export declare function rewriteBody(body: string, kind: BodyKind, currentUpstrea
57
57
  /**
58
58
  * A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
59
59
  * origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
60
+ * `browserSafe` (the server keeps its own jar, so this copy only serves the page's script):
61
+ * also no `Secure` / `SameSite`, which a plain-http hub makes the browser refuse, and no
62
+ * `__Host-` / `__Secure-` cookie, whose rules a narrowed Path breaks.
60
63
  */
61
- export declare function rewriteSetCookie(cookie: string, route: LoginProxyRoute): string;
64
+ export declare function rewriteSetCookie(cookie: string, route: LoginProxyRoute, options?: {
65
+ readonly browserSafe?: boolean;
66
+ }): string;
62
67
  /**
63
68
  * Every Set-Cookie of one vendor response, rewritten. Dropping `Domain` folds cookies
64
69
  * the vendor keeps apart (`session-id` on `.amazon.it` and on `.www.amazon.it`) into
65
70
  * one, so a past-dated set that only deleted the other domain's copy would delete the
66
71
  * live one the same response just set: that deletion is dropped.
67
72
  */
68
- export declare function rewriteSetCookies(cookies: readonly string[], route: LoginProxyRoute): string[];
73
+ export declare function rewriteSetCookies(cookies: readonly string[], route: LoginProxyRoute, options?: {
74
+ readonly browserSafe?: boolean;
75
+ }): string[];
69
76
  /**
70
77
  * A browser `Referer` that points into this proxy, turned back into the vendor URL it
71
78
  * stands for. A Referer that is not ours is dropped (null): it names the hub.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/types",
3
- "version": "1.2.451",
3
+ "version": "1.2.453",
4
4
  "description": "Shared types, interfaces, and model catalogs for the CamStack detection ecosystem",
5
5
  "keywords": [
6
6
  "camstack",