@ran-sh/dsh-crew 1.9.0 → 1.10.0
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/.claude-plugin/marketplace.json +16 -16
- package/.claude-plugin/plugin.json +14 -14
- package/LICENSE +21 -21
- package/README.de.md +359 -359
- package/README.es.md +359 -359
- package/README.fr.md +359 -359
- package/README.hi.md +359 -359
- package/README.id.md +359 -359
- package/README.ja.md +359 -359
- package/README.ko.md +359 -359
- package/README.md +126 -126
- package/README.pt.md +359 -359
- package/README.ru.md +359 -359
- package/README.th.md +359 -359
- package/README.tr.md +359 -359
- package/README.vi.md +359 -359
- package/README.zh-TW.md +359 -359
- package/README.zh.md +116 -116
- package/agents/ds-flash.md +24 -24
- package/agents/ds-pro.md +24 -24
- package/agents/ds-reviewer.md +23 -23
- package/agents/ds-worker.md +23 -23
- package/bin/dsh-crew.mjs +16 -16
- package/codex/agents/ds-flash.toml +34 -34
- package/codex/agents/ds-pro.toml +34 -34
- package/codex/agents/ds-reviewer.toml +32 -32
- package/codex/agents/ds-worker.toml +32 -32
- package/codex/prompts/dsh-config.md +19 -19
- package/codex/prompts/dsh-status.md +5 -5
- package/commands/config.md +23 -23
- package/commands/off.md +5 -5
- package/commands/on.md +5 -5
- package/commands/status.md +9 -9
- package/cordis.patch.yml +4 -4
- package/docs/gpt-relay-extension.md +103 -103
- package/docs/installation.md +138 -138
- package/docs/job-contracts.md +103 -103
- package/docs/readiness-matrix.md +85 -85
- package/docs/ui-surfaces.md +107 -107
- package/official-web-bridge/cordis.patch.yml +4 -4
- package/official-web-bridge/entry.mjs +1 -1
- package/official-web-bridge/overlay-entry.mjs +59 -59
- package/official-web-bridge/package.json +25 -25
- package/package.json +3 -2
- package/scripts/build-client.mjs +49 -49
- package/scripts/live-crew-smoke.mjs +39 -39
- package/scripts/live-policy-matrix.mjs +177 -177
- package/scripts/policy-probe.mjs +101 -101
- package/scripts/remove-legacy-official-bridge.ps1 +89 -89
- package/scripts/setup.mjs +393 -393
- package/scripts/smoke-real.mjs +110 -110
- package/scripts/smoke.mjs +78 -78
- package/scripts/verify-crew-ui-polish.mjs +145 -145
- package/scripts/verify-history-ui.mjs +97 -97
- package/scripts/verify-installer-fix.mjs +26 -26
- package/scripts/verify-npm-install.mjs +311 -311
- package/scripts/verify-official-bridge-e2e.mjs +192 -192
- package/src/adaptive-routing.mjs +260 -260
- package/src/client/activation-summary.tsx +64 -64
- package/src/client/collapsible-sections.mjs +55 -55
- package/src/client/history-panel.tsx +108 -108
- package/src/client/host-readiness.mjs +71 -71
- package/src/client/index.tsx +1711 -1711
- package/src/client/model-callability-view.mjs +17 -17
- package/src/client/panel-chrome.tsx +40 -40
- package/src/client/quick-entry.tsx +10 -10
- package/src/client/quick-panel.tsx +275 -275
- package/src/client/readiness-envelope.mjs +34 -34
- package/src/client/surface-detection.mjs +43 -43
- package/src/config-readiness.mjs +226 -226
- package/src/credential-reference.mjs +38 -38
- package/src/delivery.mjs +205 -205
- package/src/dsh-cli-runtime.mjs +1021 -1021
- package/src/dsh-cohort.mjs +20 -20
- package/src/extension-contract.mjs +104 -104
- package/src/failure-classification.mjs +201 -201
- package/src/history/admission-gate.mjs +67 -67
- package/src/history/archive-store.mjs +272 -272
- package/src/history/cleanup-plan.mjs +89 -89
- package/src/history/http.mjs +32 -32
- package/src/history/operation.mjs +86 -86
- package/src/history/runner-detach.mjs +34 -34
- package/src/history/runner.mjs +52 -52
- package/src/history/runtime.mjs +36 -36
- package/src/history/service.mjs +177 -177
- package/src/history/state.mjs +27 -27
- package/src/hub/entry.mjs +104 -104
- package/src/hub/index.mjs +2694 -2694
- package/src/hub-client.mjs +154 -154
- package/src/hub-compatibility.mjs +42 -42
- package/src/i18n.mjs +19 -19
- package/src/information-flow.mjs +67 -67
- package/src/install/cli.mjs +28 -28
- package/src/install/install-legacy.mjs +711 -711
- package/src/install/install.mjs +483 -483
- package/src/install/npx-lifecycle.mjs +3456 -3456
- package/src/install/official-frontend-assets.mjs +78 -78
- package/src/install/official-web.mjs +95 -95
- package/src/install/payload-content.mjs +88 -88
- package/src/install/windows-startup.mjs +236 -236
- package/src/install/windows-supervisor-adapter.mjs +443 -443
- package/src/install/windows-supervisor-lifecycle.mjs +782 -782
- package/src/install/zcode.mjs +397 -397
- package/src/job-contracts.mjs +255 -255
- package/src/local-request-guard.mjs +60 -60
- package/src/mcp-runtime.mjs +340 -340
- package/src/model-callability-contract.mjs +79 -79
- package/src/model-catalog.mjs +180 -180
- package/src/model-routing.mjs +586 -586
- package/src/model-schedule.mjs +207 -207
- package/src/official-web-bridge.mjs +447 -447
- package/src/policy.mjs +235 -235
- package/src/provider-delete-adapters.mjs +1934 -1934
- package/src/provider-health.mjs +130 -130
- package/src/provider-inventory.mjs +182 -182
- package/src/provider-layer-migration-adapters.mjs +759 -759
- package/src/provider-layer-migration.mjs +198 -198
- package/src/provider-lifecycle-state.mjs +103 -103
- package/src/provider-lifecycle.mjs +252 -252
- package/src/provider-profile-store.mjs +390 -390
- package/src/provider-settings-store.mjs +633 -633
- package/src/provider-store-lock.mjs +67 -67
- package/src/readiness-matrix.mjs +181 -181
- package/src/removable-waiter.mjs +29 -29
- package/src/role-profiles.mjs +107 -107
- package/src/runtime-controls.mjs +84 -84
- package/src/runtime-identity-contract.mjs +34 -34
- package/src/runtime-identity.mjs +235 -235
- package/src/runtime-readiness-snapshot.mjs +285 -285
- package/src/server.mjs +581 -581
- package/src/session-origins.mjs +60 -60
- package/src/standalone-sdk.mjs +23 -23
- package/src/status-shard.mjs +63 -63
- package/src/structured-error-code.mjs +38 -38
- package/src/supervisor/restart-request.mjs +256 -256
- package/src/workflow-runtime.mjs +739 -739
- package/src/workflow.mjs +155 -155
- package/src/workspace-audit.mjs +231 -231
- package/src/workspace-context.mjs +146 -146
- package/src/workspace-isolation.mjs +463 -455
- package/src/workspace-readiness.mjs +32 -32
- package/statusline/statusline.sh +14 -14
- package/statusline/worker-segment.sh +35 -35
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +1302 -1302
- package/windows/supervisor-control.ps1 +467 -467
- package/worker.cordis.yml +67 -67
- package/zcode/agents/ds-reviewer.md +31 -31
- package/zcode/agents/ds-worker.md +31 -31
- package/zcode/commands/dsh-config.md +17 -17
- package/zcode/commands/dsh-status.md +5 -5
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
// Pure planning for promoting legacy Crew profile providers into the Harness
|
|
2
|
-
// user settings layer. Planning never reads credentials or performs writes.
|
|
3
|
-
|
|
4
|
-
import { classifyCredentialReference } from './credential-reference.mjs';
|
|
5
|
-
|
|
6
|
-
const BUILTIN_PROVIDER_IDS = new Set(['deepseek-official']);
|
|
7
|
-
|
|
8
|
-
function text(value) {
|
|
9
|
-
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function authorityKind(declaration) {
|
|
13
|
-
return text(declaration?.declaration_authority?.kind);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function providerId(declaration) {
|
|
17
|
-
return text(declaration?.id);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function safeCredentialRef(declaration) {
|
|
21
|
-
const ref = declaration?.credential_ref;
|
|
22
|
-
const kind = ref && typeof ref === 'object' && typeof ref.kind === 'string' ? ref.kind : 'env';
|
|
23
|
-
return classifyCredentialReference(ref, { kind }).value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function safeProjection(declaration) {
|
|
27
|
-
return {
|
|
28
|
-
id: providerId(declaration),
|
|
29
|
-
display_name: text(declaration?.display_name) ?? providerId(declaration),
|
|
30
|
-
credential_ref: safeCredentialRef(declaration),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Build a bounded, secret-free migration plan from composition/base provider
|
|
36
|
-
* declarations to Harness user settings. The caller supplies declarations
|
|
37
|
-
* already obtained from the two authoritative stores.
|
|
38
|
-
*/
|
|
39
|
-
export function buildProviderLayerMigrationPlan({ declarations = [], catalogProviders = [], harnessDefault = null, routingReferences = [], tombstones = {}, recoveryTransactions = [], catalogEvidence = { ok: true, partial: false }, declarationEvidence = { ok: true }, lifecycleEvidence = { ok: true }, defaultEvidence = { ok: true } } = {}) {
|
|
40
|
-
const entries = Array.isArray(declarations) ? declarations : [];
|
|
41
|
-
const base = entries.filter((entry) => authorityKind(entry) === 'crew-profile' && providerId(entry));
|
|
42
|
-
const user = entries.filter((entry) => authorityKind(entry) === 'harness-settings' && providerId(entry));
|
|
43
|
-
const byBase = new Map(base.map((entry) => [providerId(entry), entry]));
|
|
44
|
-
const byUser = new Map(user.map((entry) => [providerId(entry), entry]));
|
|
45
|
-
const catalog = new Map((Array.isArray(catalogProviders) ? catalogProviders : [])
|
|
46
|
-
.map((entry) => [text(entry?.id), entry]).filter(([id]) => id));
|
|
47
|
-
const safeTombstones = tombstones && typeof tombstones === 'object' && !Array.isArray(tombstones) ? tombstones : {};
|
|
48
|
-
const safeRecoveryTransactions = Array.isArray(recoveryTransactions) ? recoveryTransactions : [];
|
|
49
|
-
const unresolvedRecovery = safeRecoveryTransactions.some((entry) => entry?.unresolved === true || !text(entry?.provider_id));
|
|
50
|
-
const pendingRecovery = safeRecoveryTransactions.length > 0;
|
|
51
|
-
const catalogUnavailable = catalogEvidence?.ok !== true || catalogEvidence?.partial === true;
|
|
52
|
-
const declarationUnavailable = declarationEvidence?.ok !== true;
|
|
53
|
-
const lifecycleUnavailable = lifecycleEvidence?.ok !== true;
|
|
54
|
-
const defaultUnavailable = defaultEvidence?.ok !== true;
|
|
55
|
-
const providers = [...new Set([...byBase.keys(), ...byUser.keys()])]
|
|
56
|
-
.filter((id) => !BUILTIN_PROVIDER_IDS.has(id))
|
|
57
|
-
.map((id) => {
|
|
58
|
-
const baseEntry = byBase.get(id) ?? null;
|
|
59
|
-
const userEntry = byUser.get(id) ?? null;
|
|
60
|
-
const catalogEntry = catalog.get(id) ?? null;
|
|
61
|
-
// Adapter ownership is a catalog fact, not a provider-name heuristic.
|
|
62
|
-
// An explicit true blocks the transition; missing metadata (including a
|
|
63
|
-
// missing catalog entry) also blocks a base-layer transition because
|
|
64
|
-
// reusing an unknown adapter id could hide the native Delete action.
|
|
65
|
-
const collision = catalogEntry?.adapter_owned === true;
|
|
66
|
-
const ownershipUnknown = baseEntry !== null && (catalogEntry === null || typeof catalogEntry.adapter_owned !== 'boolean');
|
|
67
|
-
const tombstoned = safeTombstones[id] === 'absent';
|
|
68
|
-
const recovery = safeRecoveryTransactions.find((entry) => text(entry?.provider_id) === id);
|
|
69
|
-
const blockedCode = lifecycleUnavailable
|
|
70
|
-
? 'PROVIDER_LIFECYCLE_UNAVAILABLE'
|
|
71
|
-
: catalogUnavailable
|
|
72
|
-
? 'MODEL_CATALOG_UNAVAILABLE'
|
|
73
|
-
: declarationUnavailable
|
|
74
|
-
? 'PROVIDER_SOURCE_UNRESOLVED'
|
|
75
|
-
: defaultUnavailable
|
|
76
|
-
? 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE'
|
|
77
|
-
: unresolvedRecovery
|
|
78
|
-
? 'PROVIDER_DELETE_RECOVERY_UNRESOLVED'
|
|
79
|
-
: pendingRecovery
|
|
80
|
-
? 'PROVIDER_DELETE_RECOVERY_PENDING'
|
|
81
|
-
: tombstoned
|
|
82
|
-
? 'PROVIDER_ALREADY_ABSENT'
|
|
83
|
-
: null;
|
|
84
|
-
const ownershipCode = collision
|
|
85
|
-
? 'HARNESS_PROVIDER_ID_COLLISION'
|
|
86
|
-
: ownershipUnknown ? 'HARNESS_PROVIDER_OWNERSHIP_UNAVAILABLE' : null;
|
|
87
|
-
const action = blockedCode ? 'blocked' : ownershipCode ? 'collision-review' : baseEntry && userEntry ? 'promote-existing-user' : baseEntry ? 'materialize-user' : 'none';
|
|
88
|
-
const currentNativeRemovable = userEntry !== null && baseEntry === null;
|
|
89
|
-
const migrationAction = action === 'promote-existing-user' || action === 'materialize-user';
|
|
90
|
-
return {
|
|
91
|
-
provider_id: id,
|
|
92
|
-
action,
|
|
93
|
-
current_native_removable: currentNativeRemovable,
|
|
94
|
-
target_native_removable: migrationAction ? true : currentNativeRemovable,
|
|
95
|
-
native_removable_after: migrationAction ? 'pending-verification' : ownershipCode || blockedCode ? 'unknown' : currentNativeRemovable,
|
|
96
|
-
requires_base_removal: baseEntry !== null,
|
|
97
|
-
target_user_layer: migrationAction || currentNativeRemovable,
|
|
98
|
-
source: { base: baseEntry ? safeProjection(baseEntry) : null, user: userEntry ? safeProjection(userEntry) : null },
|
|
99
|
-
credential_reference: safeCredentialRef(userEntry) ?? safeCredentialRef(baseEntry),
|
|
100
|
-
collision: ownershipCode ? { reason_code: ownershipCode, ...(ownershipCode === 'HARNESS_PROVIDER_ID_COLLISION' ? { requires_target_id: true } : {}) } : null,
|
|
101
|
-
...(blockedCode ? { blocked_reason: blockedCode } : ownershipCode ? { blocked_reason: ownershipCode } : {}),
|
|
102
|
-
referenced_by: (Array.isArray(routingReferences) ? routingReferences : [])
|
|
103
|
-
.filter((ref) => ref?.provider === id).map((ref) => ({ provider: id, model: text(ref.model) })),
|
|
104
|
-
harness_default: harnessDefault?.provider === id,
|
|
105
|
-
};
|
|
106
|
-
});
|
|
107
|
-
return {
|
|
108
|
-
schema_version: 1,
|
|
109
|
-
kind: 'provider-layer-migration-plan',
|
|
110
|
-
requires_confirmation: providers.some((entry) => entry.action !== 'none'),
|
|
111
|
-
providers: providers.filter((entry) => entry.action !== 'none'),
|
|
112
|
-
blocked: providers.filter((entry) => entry.action === 'collision-review' || entry.action === 'blocked').map((entry) => ({
|
|
113
|
-
provider_id: entry.provider_id,
|
|
114
|
-
code: entry.blocked_reason ?? 'HARNESS_PROVIDER_ID_COLLISION',
|
|
115
|
-
})),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function hasProviderLayerMigration(plan) {
|
|
120
|
-
return Array.isArray(plan?.providers) && plan.providers.length > 0;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const MIGRATION_STATES = Object.freeze(['PLANNED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING', 'VERIFIED', 'FAILED']);
|
|
124
|
-
|
|
125
|
-
function migrationCode(error, fallback = 'PROVIDER_MIGRATION_FAILED') {
|
|
126
|
-
const code = text(error?.code);
|
|
127
|
-
return code && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : fallback;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Execute one confirmed provider-layer transition through explicit adapters.
|
|
132
|
-
* Adapters own filesystem/CAS/restart details; this state machine never reads
|
|
133
|
-
* credentials and never runs for a GET or an unconfirmed request.
|
|
134
|
-
*/
|
|
135
|
-
export async function executeProviderLayerMigration(plan, hooks = {}, { confirm = false, deferRestart = false } = {}) {
|
|
136
|
-
if (confirm !== true) return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_CONFIRM_REQUIRED', transaction_id: plan?.plan_id ?? null };
|
|
137
|
-
if (!plan?.plan_id || !text(plan.provider_id) || !['promote-existing-user', 'materialize-user'].includes(plan.action)) {
|
|
138
|
-
return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_PLAN_INVALID', transaction_id: plan?.plan_id ?? null };
|
|
139
|
-
}
|
|
140
|
-
for (const name of ['backup', 'materialize', 'removeBase', 'rollback']) {
|
|
141
|
-
if (typeof hooks[name] !== 'function') return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
|
|
142
|
-
}
|
|
143
|
-
if (!deferRestart && (typeof hooks.restart !== 'function' || typeof hooks.verify !== 'function')) {
|
|
144
|
-
return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
|
|
145
|
-
}
|
|
146
|
-
let state = 'PLANNED';
|
|
147
|
-
const events = [{ state, at: new Date().toISOString() }];
|
|
148
|
-
let rollbackAttempted = false;
|
|
149
|
-
let rollbackError = null;
|
|
150
|
-
let verification = null;
|
|
151
|
-
try {
|
|
152
|
-
await hooks.backup(plan);
|
|
153
|
-
await hooks.materialize(plan);
|
|
154
|
-
await hooks.removeBase(plan);
|
|
155
|
-
state = 'APPLIED';
|
|
156
|
-
events.push({ state, at: new Date().toISOString() });
|
|
157
|
-
if (deferRestart) {
|
|
158
|
-
state = 'RESTART_PENDING';
|
|
159
|
-
events.push({ state, at: new Date().toISOString() });
|
|
160
|
-
} else {
|
|
161
|
-
const restarted = await hooks.restart(plan);
|
|
162
|
-
if (restarted?.ok === false) throw Object.assign(new Error('provider migration restart failed'), { code: restarted.code });
|
|
163
|
-
state = 'VERIFYING';
|
|
164
|
-
events.push({ state, at: new Date().toISOString() });
|
|
165
|
-
verification = await hooks.verify(plan);
|
|
166
|
-
if (verification?.nativeRemovable !== true || verification?.baseAbsent !== true || verification?.userPresent !== true) {
|
|
167
|
-
throw Object.assign(new Error('provider migration verification incomplete'), { code: 'PROVIDER_MIGRATION_VERIFY_FAILED' });
|
|
168
|
-
}
|
|
169
|
-
state = 'VERIFIED';
|
|
170
|
-
events.push({ state, at: new Date().toISOString() });
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
rollbackAttempted = true;
|
|
174
|
-
try { await hooks.rollback(plan); }
|
|
175
|
-
catch (rollbackErrorValue) { rollbackError = migrationCode(rollbackErrorValue, 'PROVIDER_MIGRATION_ROLLBACK_FAILED'); }
|
|
176
|
-
state = 'FAILED';
|
|
177
|
-
events.push({ state, at: new Date().toISOString() });
|
|
178
|
-
return {
|
|
179
|
-
ok: false,
|
|
180
|
-
state,
|
|
181
|
-
code: migrationCode(error),
|
|
182
|
-
transaction_id: plan.plan_id,
|
|
183
|
-
provider_id: plan.provider_id,
|
|
184
|
-
rollback_attempted: rollbackAttempted,
|
|
185
|
-
...(rollbackError ? { rollback_error: rollbackError } : {}),
|
|
186
|
-
events,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
ok: state === 'VERIFIED' || state === 'RESTART_PENDING',
|
|
191
|
-
state,
|
|
192
|
-
transaction_id: plan.plan_id,
|
|
193
|
-
provider_id: plan.provider_id,
|
|
194
|
-
restart_required: true,
|
|
195
|
-
...(verification ? { verification } : {}),
|
|
196
|
-
events,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
1
|
+
// Pure planning for promoting legacy Crew profile providers into the Harness
|
|
2
|
+
// user settings layer. Planning never reads credentials or performs writes.
|
|
3
|
+
|
|
4
|
+
import { classifyCredentialReference } from './credential-reference.mjs';
|
|
5
|
+
|
|
6
|
+
const BUILTIN_PROVIDER_IDS = new Set(['deepseek-official']);
|
|
7
|
+
|
|
8
|
+
function text(value) {
|
|
9
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function authorityKind(declaration) {
|
|
13
|
+
return text(declaration?.declaration_authority?.kind);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function providerId(declaration) {
|
|
17
|
+
return text(declaration?.id);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function safeCredentialRef(declaration) {
|
|
21
|
+
const ref = declaration?.credential_ref;
|
|
22
|
+
const kind = ref && typeof ref === 'object' && typeof ref.kind === 'string' ? ref.kind : 'env';
|
|
23
|
+
return classifyCredentialReference(ref, { kind }).value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeProjection(declaration) {
|
|
27
|
+
return {
|
|
28
|
+
id: providerId(declaration),
|
|
29
|
+
display_name: text(declaration?.display_name) ?? providerId(declaration),
|
|
30
|
+
credential_ref: safeCredentialRef(declaration),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a bounded, secret-free migration plan from composition/base provider
|
|
36
|
+
* declarations to Harness user settings. The caller supplies declarations
|
|
37
|
+
* already obtained from the two authoritative stores.
|
|
38
|
+
*/
|
|
39
|
+
export function buildProviderLayerMigrationPlan({ declarations = [], catalogProviders = [], harnessDefault = null, routingReferences = [], tombstones = {}, recoveryTransactions = [], catalogEvidence = { ok: true, partial: false }, declarationEvidence = { ok: true }, lifecycleEvidence = { ok: true }, defaultEvidence = { ok: true } } = {}) {
|
|
40
|
+
const entries = Array.isArray(declarations) ? declarations : [];
|
|
41
|
+
const base = entries.filter((entry) => authorityKind(entry) === 'crew-profile' && providerId(entry));
|
|
42
|
+
const user = entries.filter((entry) => authorityKind(entry) === 'harness-settings' && providerId(entry));
|
|
43
|
+
const byBase = new Map(base.map((entry) => [providerId(entry), entry]));
|
|
44
|
+
const byUser = new Map(user.map((entry) => [providerId(entry), entry]));
|
|
45
|
+
const catalog = new Map((Array.isArray(catalogProviders) ? catalogProviders : [])
|
|
46
|
+
.map((entry) => [text(entry?.id), entry]).filter(([id]) => id));
|
|
47
|
+
const safeTombstones = tombstones && typeof tombstones === 'object' && !Array.isArray(tombstones) ? tombstones : {};
|
|
48
|
+
const safeRecoveryTransactions = Array.isArray(recoveryTransactions) ? recoveryTransactions : [];
|
|
49
|
+
const unresolvedRecovery = safeRecoveryTransactions.some((entry) => entry?.unresolved === true || !text(entry?.provider_id));
|
|
50
|
+
const pendingRecovery = safeRecoveryTransactions.length > 0;
|
|
51
|
+
const catalogUnavailable = catalogEvidence?.ok !== true || catalogEvidence?.partial === true;
|
|
52
|
+
const declarationUnavailable = declarationEvidence?.ok !== true;
|
|
53
|
+
const lifecycleUnavailable = lifecycleEvidence?.ok !== true;
|
|
54
|
+
const defaultUnavailable = defaultEvidence?.ok !== true;
|
|
55
|
+
const providers = [...new Set([...byBase.keys(), ...byUser.keys()])]
|
|
56
|
+
.filter((id) => !BUILTIN_PROVIDER_IDS.has(id))
|
|
57
|
+
.map((id) => {
|
|
58
|
+
const baseEntry = byBase.get(id) ?? null;
|
|
59
|
+
const userEntry = byUser.get(id) ?? null;
|
|
60
|
+
const catalogEntry = catalog.get(id) ?? null;
|
|
61
|
+
// Adapter ownership is a catalog fact, not a provider-name heuristic.
|
|
62
|
+
// An explicit true blocks the transition; missing metadata (including a
|
|
63
|
+
// missing catalog entry) also blocks a base-layer transition because
|
|
64
|
+
// reusing an unknown adapter id could hide the native Delete action.
|
|
65
|
+
const collision = catalogEntry?.adapter_owned === true;
|
|
66
|
+
const ownershipUnknown = baseEntry !== null && (catalogEntry === null || typeof catalogEntry.adapter_owned !== 'boolean');
|
|
67
|
+
const tombstoned = safeTombstones[id] === 'absent';
|
|
68
|
+
const recovery = safeRecoveryTransactions.find((entry) => text(entry?.provider_id) === id);
|
|
69
|
+
const blockedCode = lifecycleUnavailable
|
|
70
|
+
? 'PROVIDER_LIFECYCLE_UNAVAILABLE'
|
|
71
|
+
: catalogUnavailable
|
|
72
|
+
? 'MODEL_CATALOG_UNAVAILABLE'
|
|
73
|
+
: declarationUnavailable
|
|
74
|
+
? 'PROVIDER_SOURCE_UNRESOLVED'
|
|
75
|
+
: defaultUnavailable
|
|
76
|
+
? 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE'
|
|
77
|
+
: unresolvedRecovery
|
|
78
|
+
? 'PROVIDER_DELETE_RECOVERY_UNRESOLVED'
|
|
79
|
+
: pendingRecovery
|
|
80
|
+
? 'PROVIDER_DELETE_RECOVERY_PENDING'
|
|
81
|
+
: tombstoned
|
|
82
|
+
? 'PROVIDER_ALREADY_ABSENT'
|
|
83
|
+
: null;
|
|
84
|
+
const ownershipCode = collision
|
|
85
|
+
? 'HARNESS_PROVIDER_ID_COLLISION'
|
|
86
|
+
: ownershipUnknown ? 'HARNESS_PROVIDER_OWNERSHIP_UNAVAILABLE' : null;
|
|
87
|
+
const action = blockedCode ? 'blocked' : ownershipCode ? 'collision-review' : baseEntry && userEntry ? 'promote-existing-user' : baseEntry ? 'materialize-user' : 'none';
|
|
88
|
+
const currentNativeRemovable = userEntry !== null && baseEntry === null;
|
|
89
|
+
const migrationAction = action === 'promote-existing-user' || action === 'materialize-user';
|
|
90
|
+
return {
|
|
91
|
+
provider_id: id,
|
|
92
|
+
action,
|
|
93
|
+
current_native_removable: currentNativeRemovable,
|
|
94
|
+
target_native_removable: migrationAction ? true : currentNativeRemovable,
|
|
95
|
+
native_removable_after: migrationAction ? 'pending-verification' : ownershipCode || blockedCode ? 'unknown' : currentNativeRemovable,
|
|
96
|
+
requires_base_removal: baseEntry !== null,
|
|
97
|
+
target_user_layer: migrationAction || currentNativeRemovable,
|
|
98
|
+
source: { base: baseEntry ? safeProjection(baseEntry) : null, user: userEntry ? safeProjection(userEntry) : null },
|
|
99
|
+
credential_reference: safeCredentialRef(userEntry) ?? safeCredentialRef(baseEntry),
|
|
100
|
+
collision: ownershipCode ? { reason_code: ownershipCode, ...(ownershipCode === 'HARNESS_PROVIDER_ID_COLLISION' ? { requires_target_id: true } : {}) } : null,
|
|
101
|
+
...(blockedCode ? { blocked_reason: blockedCode } : ownershipCode ? { blocked_reason: ownershipCode } : {}),
|
|
102
|
+
referenced_by: (Array.isArray(routingReferences) ? routingReferences : [])
|
|
103
|
+
.filter((ref) => ref?.provider === id).map((ref) => ({ provider: id, model: text(ref.model) })),
|
|
104
|
+
harness_default: harnessDefault?.provider === id,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
schema_version: 1,
|
|
109
|
+
kind: 'provider-layer-migration-plan',
|
|
110
|
+
requires_confirmation: providers.some((entry) => entry.action !== 'none'),
|
|
111
|
+
providers: providers.filter((entry) => entry.action !== 'none'),
|
|
112
|
+
blocked: providers.filter((entry) => entry.action === 'collision-review' || entry.action === 'blocked').map((entry) => ({
|
|
113
|
+
provider_id: entry.provider_id,
|
|
114
|
+
code: entry.blocked_reason ?? 'HARNESS_PROVIDER_ID_COLLISION',
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function hasProviderLayerMigration(plan) {
|
|
120
|
+
return Array.isArray(plan?.providers) && plan.providers.length > 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const MIGRATION_STATES = Object.freeze(['PLANNED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING', 'VERIFIED', 'FAILED']);
|
|
124
|
+
|
|
125
|
+
function migrationCode(error, fallback = 'PROVIDER_MIGRATION_FAILED') {
|
|
126
|
+
const code = text(error?.code);
|
|
127
|
+
return code && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : fallback;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execute one confirmed provider-layer transition through explicit adapters.
|
|
132
|
+
* Adapters own filesystem/CAS/restart details; this state machine never reads
|
|
133
|
+
* credentials and never runs for a GET or an unconfirmed request.
|
|
134
|
+
*/
|
|
135
|
+
export async function executeProviderLayerMigration(plan, hooks = {}, { confirm = false, deferRestart = false } = {}) {
|
|
136
|
+
if (confirm !== true) return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_CONFIRM_REQUIRED', transaction_id: plan?.plan_id ?? null };
|
|
137
|
+
if (!plan?.plan_id || !text(plan.provider_id) || !['promote-existing-user', 'materialize-user'].includes(plan.action)) {
|
|
138
|
+
return { ok: false, state: 'BLOCKED', code: 'PROVIDER_MIGRATION_PLAN_INVALID', transaction_id: plan?.plan_id ?? null };
|
|
139
|
+
}
|
|
140
|
+
for (const name of ['backup', 'materialize', 'removeBase', 'rollback']) {
|
|
141
|
+
if (typeof hooks[name] !== 'function') return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
|
|
142
|
+
}
|
|
143
|
+
if (!deferRestart && (typeof hooks.restart !== 'function' || typeof hooks.verify !== 'function')) {
|
|
144
|
+
return { ok: false, state: 'FAILED', code: 'PROVIDER_MIGRATION_HOOK_MISSING', transaction_id: plan.plan_id };
|
|
145
|
+
}
|
|
146
|
+
let state = 'PLANNED';
|
|
147
|
+
const events = [{ state, at: new Date().toISOString() }];
|
|
148
|
+
let rollbackAttempted = false;
|
|
149
|
+
let rollbackError = null;
|
|
150
|
+
let verification = null;
|
|
151
|
+
try {
|
|
152
|
+
await hooks.backup(plan);
|
|
153
|
+
await hooks.materialize(plan);
|
|
154
|
+
await hooks.removeBase(plan);
|
|
155
|
+
state = 'APPLIED';
|
|
156
|
+
events.push({ state, at: new Date().toISOString() });
|
|
157
|
+
if (deferRestart) {
|
|
158
|
+
state = 'RESTART_PENDING';
|
|
159
|
+
events.push({ state, at: new Date().toISOString() });
|
|
160
|
+
} else {
|
|
161
|
+
const restarted = await hooks.restart(plan);
|
|
162
|
+
if (restarted?.ok === false) throw Object.assign(new Error('provider migration restart failed'), { code: restarted.code });
|
|
163
|
+
state = 'VERIFYING';
|
|
164
|
+
events.push({ state, at: new Date().toISOString() });
|
|
165
|
+
verification = await hooks.verify(plan);
|
|
166
|
+
if (verification?.nativeRemovable !== true || verification?.baseAbsent !== true || verification?.userPresent !== true) {
|
|
167
|
+
throw Object.assign(new Error('provider migration verification incomplete'), { code: 'PROVIDER_MIGRATION_VERIFY_FAILED' });
|
|
168
|
+
}
|
|
169
|
+
state = 'VERIFIED';
|
|
170
|
+
events.push({ state, at: new Date().toISOString() });
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
rollbackAttempted = true;
|
|
174
|
+
try { await hooks.rollback(plan); }
|
|
175
|
+
catch (rollbackErrorValue) { rollbackError = migrationCode(rollbackErrorValue, 'PROVIDER_MIGRATION_ROLLBACK_FAILED'); }
|
|
176
|
+
state = 'FAILED';
|
|
177
|
+
events.push({ state, at: new Date().toISOString() });
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
state,
|
|
181
|
+
code: migrationCode(error),
|
|
182
|
+
transaction_id: plan.plan_id,
|
|
183
|
+
provider_id: plan.provider_id,
|
|
184
|
+
rollback_attempted: rollbackAttempted,
|
|
185
|
+
...(rollbackError ? { rollback_error: rollbackError } : {}),
|
|
186
|
+
events,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
ok: state === 'VERIFIED' || state === 'RESTART_PENDING',
|
|
191
|
+
state,
|
|
192
|
+
transaction_id: plan.plan_id,
|
|
193
|
+
provider_id: plan.provider_id,
|
|
194
|
+
restart_required: true,
|
|
195
|
+
...(verification ? { verification } : {}),
|
|
196
|
+
events,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
// Persistent, secret-free lifecycle intent for Harness provider management.
|
|
2
|
-
// Tombstones prevent an explicitly removed managed route from being re-seeded;
|
|
3
|
-
// transactions retain only bounded audit metadata needed for recovery.
|
|
4
|
-
|
|
5
|
-
import { classifyCredentialReference } from './credential-reference.mjs';
|
|
6
|
-
|
|
7
|
-
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
|
-
const REVISION_PATTERN = /^[a-f0-9]{64}$/;
|
|
9
|
-
const STATES = new Set([
|
|
10
|
-
'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
|
|
11
|
-
'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
function validId(value) {
|
|
15
|
-
return typeof value === 'string' && ID_PATTERN.test(value);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function validRevision(value) {
|
|
19
|
-
return typeof value === 'string' && REVISION_PATTERN.test(value);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function validTimestamp(value) {
|
|
23
|
-
return typeof value === 'string' && value.length >= 16 && value.length <= 64;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalizedCredentialRefs(value) {
|
|
27
|
-
if (!Array.isArray(value)) return undefined;
|
|
28
|
-
const refs = value.map((ref) => {
|
|
29
|
-
if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return null;
|
|
30
|
-
const kind = typeof ref.kind === 'string' && ref.kind.trim() ? ref.kind.trim() : null;
|
|
31
|
-
const name = typeof ref.name_or_handle === 'string' && ref.name_or_handle.trim() ? ref.name_or_handle.trim() : null;
|
|
32
|
-
const ownership = typeof ref.ownership === 'string' && ref.ownership.trim() ? ref.ownership.trim() : null;
|
|
33
|
-
const safeName = classifyCredentialReference(name, { kind }).value;
|
|
34
|
-
return kind && safeName && ownership ? { kind, name_or_handle: safeName, ownership } : null;
|
|
35
|
-
}).filter(Boolean).slice(0, 32);
|
|
36
|
-
return refs;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function clone(value) {
|
|
40
|
-
return JSON.parse(JSON.stringify(value));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function normalizedTransaction(value) {
|
|
44
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
45
|
-
if (!validId(value.provider_id) || typeof value.state !== 'string' || !STATES.has(value.state)) return null;
|
|
46
|
-
const refs = normalizedCredentialRefs(value.credential_refs);
|
|
47
|
-
return {
|
|
48
|
-
provider_id: value.provider_id,
|
|
49
|
-
state: value.state,
|
|
50
|
-
...(validTimestamp(value.updated_at) ? { updated_at: value.updated_at } : {}),
|
|
51
|
-
...(validRevision(value.expected_revision) ? { expected_revision: value.expected_revision } : {}),
|
|
52
|
-
...(refs === undefined ? {} : { credential_refs: refs }),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Normalize persisted lifecycle metadata; unknown fields and secret values are discarded. */
|
|
57
|
-
export function normalizeProviderLifecycleState(input = {}) {
|
|
58
|
-
const out = { schema_version: 1, tombstones: {}, transactions: {}, last_verified_revision: {} };
|
|
59
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) return out;
|
|
60
|
-
if (input.tombstones && typeof input.tombstones === 'object' && !Array.isArray(input.tombstones)) {
|
|
61
|
-
for (const [id, value] of Object.entries(input.tombstones)) if (validId(id) && value === 'absent') out.tombstones[id] = 'absent';
|
|
62
|
-
}
|
|
63
|
-
if (input.transactions && typeof input.transactions === 'object' && !Array.isArray(input.transactions)) {
|
|
64
|
-
for (const [transactionId, value] of Object.entries(input.transactions)) {
|
|
65
|
-
if (!validId(transactionId)) continue;
|
|
66
|
-
const transaction = normalizedTransaction(value);
|
|
67
|
-
if (transaction) out.transactions[transactionId] = transaction;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (input.last_verified_revision && typeof input.last_verified_revision === 'object' && !Array.isArray(input.last_verified_revision)) {
|
|
71
|
-
for (const [id, revision] of Object.entries(input.last_verified_revision)) if (validId(id) && validRevision(revision)) out.last_verified_revision[id] = revision;
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function requireProviderId(providerId) {
|
|
77
|
-
if (!validId(providerId)) throw new TypeError('provider id must be a bounded identifier');
|
|
78
|
-
return providerId;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function markProviderTombstone(state, providerId) {
|
|
82
|
-
const id = requireProviderId(providerId);
|
|
83
|
-
const out = normalizeProviderLifecycleState(state);
|
|
84
|
-
out.tombstones[id] = 'absent';
|
|
85
|
-
return out;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function clearProviderTombstone(state, providerId) {
|
|
89
|
-
const id = requireProviderId(providerId);
|
|
90
|
-
const out = normalizeProviderLifecycleState(state);
|
|
91
|
-
delete out.tombstones[id];
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Record non-secret transaction metadata and credential reference names only. */
|
|
96
|
-
export function recordProviderTransaction(state, transaction = {}) {
|
|
97
|
-
const transactionId = requireProviderId(transaction.transaction_id ?? transaction.plan_id);
|
|
98
|
-
const normalized = normalizedTransaction(transaction);
|
|
99
|
-
if (!normalized) throw new TypeError('provider transaction metadata is invalid');
|
|
100
|
-
const out = normalizeProviderLifecycleState(state);
|
|
101
|
-
out.transactions[transactionId] = clone(normalized);
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
1
|
+
// Persistent, secret-free lifecycle intent for Harness provider management.
|
|
2
|
+
// Tombstones prevent an explicitly removed managed route from being re-seeded;
|
|
3
|
+
// transactions retain only bounded audit metadata needed for recovery.
|
|
4
|
+
|
|
5
|
+
import { classifyCredentialReference } from './credential-reference.mjs';
|
|
6
|
+
|
|
7
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
|
+
const REVISION_PATTERN = /^[a-f0-9]{64}$/;
|
|
9
|
+
const STATES = new Set([
|
|
10
|
+
'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
|
|
11
|
+
'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function validId(value) {
|
|
15
|
+
return typeof value === 'string' && ID_PATTERN.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validRevision(value) {
|
|
19
|
+
return typeof value === 'string' && REVISION_PATTERN.test(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validTimestamp(value) {
|
|
23
|
+
return typeof value === 'string' && value.length >= 16 && value.length <= 64;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizedCredentialRefs(value) {
|
|
27
|
+
if (!Array.isArray(value)) return undefined;
|
|
28
|
+
const refs = value.map((ref) => {
|
|
29
|
+
if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return null;
|
|
30
|
+
const kind = typeof ref.kind === 'string' && ref.kind.trim() ? ref.kind.trim() : null;
|
|
31
|
+
const name = typeof ref.name_or_handle === 'string' && ref.name_or_handle.trim() ? ref.name_or_handle.trim() : null;
|
|
32
|
+
const ownership = typeof ref.ownership === 'string' && ref.ownership.trim() ? ref.ownership.trim() : null;
|
|
33
|
+
const safeName = classifyCredentialReference(name, { kind }).value;
|
|
34
|
+
return kind && safeName && ownership ? { kind, name_or_handle: safeName, ownership } : null;
|
|
35
|
+
}).filter(Boolean).slice(0, 32);
|
|
36
|
+
return refs;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clone(value) {
|
|
40
|
+
return JSON.parse(JSON.stringify(value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizedTransaction(value) {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
45
|
+
if (!validId(value.provider_id) || typeof value.state !== 'string' || !STATES.has(value.state)) return null;
|
|
46
|
+
const refs = normalizedCredentialRefs(value.credential_refs);
|
|
47
|
+
return {
|
|
48
|
+
provider_id: value.provider_id,
|
|
49
|
+
state: value.state,
|
|
50
|
+
...(validTimestamp(value.updated_at) ? { updated_at: value.updated_at } : {}),
|
|
51
|
+
...(validRevision(value.expected_revision) ? { expected_revision: value.expected_revision } : {}),
|
|
52
|
+
...(refs === undefined ? {} : { credential_refs: refs }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Normalize persisted lifecycle metadata; unknown fields and secret values are discarded. */
|
|
57
|
+
export function normalizeProviderLifecycleState(input = {}) {
|
|
58
|
+
const out = { schema_version: 1, tombstones: {}, transactions: {}, last_verified_revision: {} };
|
|
59
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return out;
|
|
60
|
+
if (input.tombstones && typeof input.tombstones === 'object' && !Array.isArray(input.tombstones)) {
|
|
61
|
+
for (const [id, value] of Object.entries(input.tombstones)) if (validId(id) && value === 'absent') out.tombstones[id] = 'absent';
|
|
62
|
+
}
|
|
63
|
+
if (input.transactions && typeof input.transactions === 'object' && !Array.isArray(input.transactions)) {
|
|
64
|
+
for (const [transactionId, value] of Object.entries(input.transactions)) {
|
|
65
|
+
if (!validId(transactionId)) continue;
|
|
66
|
+
const transaction = normalizedTransaction(value);
|
|
67
|
+
if (transaction) out.transactions[transactionId] = transaction;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (input.last_verified_revision && typeof input.last_verified_revision === 'object' && !Array.isArray(input.last_verified_revision)) {
|
|
71
|
+
for (const [id, revision] of Object.entries(input.last_verified_revision)) if (validId(id) && validRevision(revision)) out.last_verified_revision[id] = revision;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requireProviderId(providerId) {
|
|
77
|
+
if (!validId(providerId)) throw new TypeError('provider id must be a bounded identifier');
|
|
78
|
+
return providerId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function markProviderTombstone(state, providerId) {
|
|
82
|
+
const id = requireProviderId(providerId);
|
|
83
|
+
const out = normalizeProviderLifecycleState(state);
|
|
84
|
+
out.tombstones[id] = 'absent';
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function clearProviderTombstone(state, providerId) {
|
|
89
|
+
const id = requireProviderId(providerId);
|
|
90
|
+
const out = normalizeProviderLifecycleState(state);
|
|
91
|
+
delete out.tombstones[id];
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Record non-secret transaction metadata and credential reference names only. */
|
|
96
|
+
export function recordProviderTransaction(state, transaction = {}) {
|
|
97
|
+
const transactionId = requireProviderId(transaction.transaction_id ?? transaction.plan_id);
|
|
98
|
+
const normalized = normalizedTransaction(transaction);
|
|
99
|
+
if (!normalized) throw new TypeError('provider transaction metadata is invalid');
|
|
100
|
+
const out = normalizeProviderLifecycleState(state);
|
|
101
|
+
out.transactions[transactionId] = clone(normalized);
|
|
102
|
+
return out;
|
|
103
|
+
}
|