@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.
- package/CHANGELOG.md +21 -0
- package/dist/addon/base-addon.d.ts +27 -0
- package/dist/addon.js +8 -3
- package/dist/addon.mjs +8 -3
- package/dist/automation/condition-eval.d.ts +20 -5
- package/dist/automation/detection-subject.d.ts +154 -0
- package/dist/automation/detection-trigger.d.ts +16 -6
- package/dist/automation/index.d.ts +10 -4
- package/dist/automation/legacy-recipe.d.ts +130 -0
- package/dist/automation/normalize-units.d.ts +4 -3
- package/dist/automation/recipe.d.ts +286 -142
- package/dist/automation/stationary-trigger.d.ts +4 -4
- package/dist/automation/step-runner.d.ts +3 -1
- package/dist/automation/trace.d.ts +51 -6
- package/dist/automation/when.d.ts +295 -0
- package/dist/capabilities/actions.cap.d.ts +10 -0
- package/dist/capabilities/addon-settings.cap.d.ts +18 -0
- package/dist/capabilities/core-blocks.cap.d.ts +2546 -51
- package/dist/capabilities/device-discovery.cap.d.ts +2 -0
- package/dist/capabilities/device-manager.cap.d.ts +16 -1
- package/dist/capabilities/device-provider.cap.d.ts +165 -0
- package/dist/capabilities/index.d.ts +3 -3
- package/dist/capabilities/integrations.cap.d.ts +1 -0
- package/dist/capabilities/keychain.cap.d.ts +12 -0
- package/dist/capabilities/local-network.cap.d.ts +198 -0
- package/dist/capabilities/storage-admin.cap.d.ts +155 -0
- package/dist/capabilities/terminal-session.cap.d.ts +7 -0
- package/dist/client.js +8 -1
- package/dist/client.mjs +8 -1
- package/dist/device/base-device-provider.d.ts +17 -0
- package/dist/generated/addon-api.d.ts +84 -0
- package/dist/generated/collection-array-methods.d.ts +1 -1
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/system-proxy.d.ts +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1969 -706
- package/dist/index.mjs +1934 -703
- package/dist/interfaces/addon.d.ts +16 -0
- package/dist/interfaces/config-ui-secrets.d.ts +5 -3
- package/dist/interfaces/config-ui.d.ts +34 -1
- package/dist/{sleep-BSPhInRm.js → sleep-CewR0zRz.js} +125 -6
- package/dist/{sleep-63gM_1JR.mjs → sleep-CeyhrcOk.mjs} +125 -6
- package/dist/vendor-login-proxy/cookie-jar.d.ts +30 -0
- package/dist/vendor-login-proxy/index.d.ts +1 -0
- package/dist/vendor-login-proxy/login-proxy.d.ts +6 -0
- package/dist/vendor-login-proxy/url-rewrite.d.ts +9 -2
- 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
|
|
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
|
|
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
|
|
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.
|