@ran-sh/dsh-crew 1.0.0 → 1.0.2
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/README.md +24 -20
- package/README.zh.md +21 -18
- package/lib/client.js +156 -13
- package/official-web-bridge/lib/client.js +156 -13
- package/package.json +166 -166
- package/src/client/index.tsx +765 -716
- package/src/hub/index.mjs +420 -66
- package/src/install/install-legacy.mjs +184 -185
- package/src/policy-legacy.mjs +2 -2
- package/src/provider-delete-adapters.mjs +1786 -451
- package/src/provider-inventory.mjs +170 -132
- package/src/provider-lifecycle-state.mjs +100 -95
- package/src/provider-lifecycle.mjs +252 -214
- package/src/provider-profile-store.mjs +189 -171
- package/src/provider-settings-store.mjs +215 -0
- package/src/runtime-identity.mjs +72 -72
|
@@ -1,132 +1,170 @@
|
|
|
1
|
-
// Secret-free inventory projection for Harness providers.
|
|
2
|
-
//
|
|
3
|
-
// Harness owns the live provider/model catalog; Crew owns the routing policy
|
|
4
|
-
// and (where explicitly marked) profile provenance. This module only joins
|
|
5
|
-
// those observations into a bounded record set for lifecycle/readiness UIs.
|
|
6
|
-
|
|
7
|
-
const LIFECYCLE_ORIGINS = new Set(['builtin', 'profile-managed', 'dynamic', 'unknown']);
|
|
8
|
-
const OWNERSHIPS = new Set(['harness', 'crew-managed-profile', 'user-managed-profile', 'dynamic-user', 'unknown']);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1
|
+
// Secret-free inventory projection for Harness providers.
|
|
2
|
+
//
|
|
3
|
+
// Harness owns the live provider/model catalog; Crew owns the routing policy
|
|
4
|
+
// and (where explicitly marked) profile provenance. This module only joins
|
|
5
|
+
// those observations into a bounded record set for lifecycle/readiness UIs.
|
|
6
|
+
|
|
7
|
+
const LIFECYCLE_ORIGINS = new Set(['builtin', 'profile-managed', 'dynamic', 'unknown']);
|
|
8
|
+
const OWNERSHIPS = new Set(['harness', 'crew-managed-profile', 'user-managed-profile', 'dynamic-user', 'unknown']);
|
|
9
|
+
const MUTABLE_AUTHORITY_KINDS = new Set(['crew-profile', 'harness-settings']);
|
|
10
|
+
export const IMMUTABLE_HARNESS_PROVIDER_IDS = Object.freeze(['deepseek-official']);
|
|
11
|
+
|
|
12
|
+
function text(value) {
|
|
13
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeProviderId(value) {
|
|
17
|
+
return text(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeCredentialRefs(declarations) {
|
|
21
|
+
const entries = Array.isArray(declarations) ? declarations : [declarations];
|
|
22
|
+
const seen = new Map();
|
|
23
|
+
const refs = [];
|
|
24
|
+
for (const declaration of entries) {
|
|
25
|
+
const raw = declaration?.credential_refs ?? declaration?.credential_ref;
|
|
26
|
+
const values = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];
|
|
27
|
+
for (const candidate of values) {
|
|
28
|
+
const name = text(typeof candidate === 'object' ? candidate.name_or_handle ?? candidate.name : candidate);
|
|
29
|
+
const kind = text(typeof candidate === 'object' ? candidate.kind : null) ?? 'env';
|
|
30
|
+
const ownership = text(typeof candidate === 'object' ? candidate.ownership : null) ?? 'crew';
|
|
31
|
+
const normalized = {
|
|
32
|
+
kind: ['env', 'crew-store', 'harness-store', 'unknown'].includes(kind) ? kind : 'unknown',
|
|
33
|
+
name_or_handle: name,
|
|
34
|
+
ownership: ['crew', 'user', 'external', 'unknown'].includes(ownership) ? ownership : 'unknown',
|
|
35
|
+
};
|
|
36
|
+
if (!name) continue;
|
|
37
|
+
const key = `${normalized.kind}:${normalized.name_or_handle}`;
|
|
38
|
+
const existing = seen.get(key);
|
|
39
|
+
if (existing) {
|
|
40
|
+
if (existing.ownership !== normalized.ownership) existing.ownership = 'unknown';
|
|
41
|
+
} else {
|
|
42
|
+
seen.set(key, normalized);
|
|
43
|
+
refs.push(normalized);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return refs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findPriorityIndex(policy, key, providerId) {
|
|
51
|
+
const list = Array.isArray(policy?.[key]) ? policy[key] : [];
|
|
52
|
+
const index = list.findIndex((entry) => normalizeProviderId(entry?.provider) === providerId);
|
|
53
|
+
return index >= 0 ? index : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function declarationsFor(declarations, id) {
|
|
57
|
+
return declarations.filter((entry) => normalizeProviderId(entry?.id) === id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeAuthority(value) {
|
|
61
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
62
|
+
const kind = text(value.kind);
|
|
63
|
+
const locator = text(value.locator);
|
|
64
|
+
return kind && locator ? { kind, locator } : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function catalogFor(providers, id) {
|
|
68
|
+
return providers.find((entry) => normalizeProviderId(entry?.id) === id) ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeOrigin(value) {
|
|
72
|
+
const origin = text(value) ?? 'unknown';
|
|
73
|
+
return LIFECYCLE_ORIGINS.has(origin) ? origin : 'unknown';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeOwnership(value) {
|
|
77
|
+
const ownership = text(value) ?? 'unknown';
|
|
78
|
+
return OWNERSHIPS.has(ownership) ? ownership : 'unknown';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function modelIds(provider) {
|
|
82
|
+
if (!Array.isArray(provider?.models)) return [];
|
|
83
|
+
return [...new Set(provider.models.map((model) => text(typeof model === 'object' ? model?.id : model)).filter(Boolean))];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a secret-free provider inventory from live Harness and Crew-owned
|
|
88
|
+
* observations. No credential value is accepted or copied into the result.
|
|
89
|
+
*/
|
|
90
|
+
export function buildProviderInventory({ catalog = {}, declarations = [], policy = {}, tombstones = {}, activeJobs = [], multimodalRefs = {}, additionalProviderIds = [] } = {}) {
|
|
91
|
+
const catalogProviders = Array.isArray(catalog?.providers) ? catalog.providers : [];
|
|
92
|
+
const safeDeclarations = Array.isArray(declarations) ? declarations : [];
|
|
93
|
+
const ids = [];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const source of [catalogProviders, safeDeclarations, Array.isArray(additionalProviderIds) ? additionalProviderIds.map((id) => ({ id })) : []]) {
|
|
96
|
+
for (const entry of source) {
|
|
97
|
+
const id = normalizeProviderId(entry?.id);
|
|
98
|
+
if (!id || seen.has(id)) continue;
|
|
99
|
+
seen.add(id);
|
|
100
|
+
ids.push(id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const harnessDefault = normalizeProviderId(catalog?.harness_default?.provider);
|
|
105
|
+
const records = ids.map((id) => {
|
|
106
|
+
const catalogEntry = catalogFor(catalogProviders, id);
|
|
107
|
+
const providerDeclarations = declarationsFor(safeDeclarations, id);
|
|
108
|
+
const declaration = providerDeclarations[0] ?? null;
|
|
109
|
+
const declared = declaration !== null;
|
|
110
|
+
const catalogued = catalogEntry !== null;
|
|
111
|
+
const tombstoned = tombstones?.[id] === 'absent';
|
|
112
|
+
const configured = declared;
|
|
113
|
+
const active = Array.isArray(activeJobs)
|
|
114
|
+
? activeJobs.filter((job) => normalizeProviderId(job?.provider) === id
|
|
115
|
+
&& !['done', 'failed', 'cancelled'].includes(job?.status)).length
|
|
116
|
+
: Number.isInteger(activeJobs?.[id]) ? Math.max(0, activeJobs[id]) : 0;
|
|
117
|
+
const multimodal = Array.isArray(multimodalRefs?.[id]) ? multimodalRefs[id].length : Number.isInteger(multimodalRefs?.[id]) ? Math.max(0, multimodalRefs[id]) : 0;
|
|
118
|
+
const declarationFile = text(declaration?.file);
|
|
119
|
+
const locator = text(declaration?.locator);
|
|
120
|
+
|
|
121
|
+
const authorities = providerDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)).filter(Boolean);
|
|
122
|
+
const uniqueAuthorities = [...new Map(authorities.map((authority) => [`${authority.kind}:${authority.locator}`, authority])).values()];
|
|
123
|
+
const mutableAuthorities = uniqueAuthorities.filter((authority) => MUTABLE_AUTHORITY_KINDS.has(authority.kind));
|
|
124
|
+
const immutable = IMMUTABLE_HARNESS_PROVIDER_IDS.includes(id);
|
|
125
|
+
const allAuthoritiesKnown = providerDeclarations.length > 0
|
|
126
|
+
&& uniqueAuthorities.length === providerDeclarations.length
|
|
127
|
+
&& uniqueAuthorities.every((authority) => MUTABLE_AUTHORITY_KINDS.has(authority.kind));
|
|
128
|
+
const deleteCapability = immutable ? 'immutable-builtin' : allAuthoritiesKnown && mutableAuthorities.length > 0 ? 'supported' : 'source-unresolved';
|
|
129
|
+
const deleteBlocker = immutable ? 'PROVIDER_BUILTIN_IMMUTABLE' : deleteCapability === 'supported' ? null : 'PROVIDER_DELETE_SOURCE_UNRESOLVED';
|
|
130
|
+
const origin = normalizeOrigin(declaration?.origin ?? (immutable ? 'builtin' : declared ? 'profile-managed' : 'unknown'));
|
|
131
|
+
const ownership = normalizeOwnership(declaration?.ownership ?? (immutable ? 'harness' : declared ? 'crew-managed-profile' : 'unknown'));
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
display_name: text(declaration?.display_name) ?? text(declaration?.name) ?? text(catalogEntry?.name) ?? id,
|
|
135
|
+
origin,
|
|
136
|
+
ownership,
|
|
137
|
+
declaration: {
|
|
138
|
+
...(declarationFile ? { file: declarationFile } : {}),
|
|
139
|
+
...(locator ? { locator } : {}),
|
|
140
|
+
...(normalizeAuthority(declaration?.declaration_authority) ? { authority: normalizeAuthority(declaration.declaration_authority) } : {}),
|
|
141
|
+
present: declared,
|
|
142
|
+
},
|
|
143
|
+
declaration_authorities: uniqueAuthorities,
|
|
144
|
+
delete_capability: deleteCapability,
|
|
145
|
+
...(deleteBlocker ? { delete_blocker: deleteBlocker } : {}),
|
|
146
|
+
models: modelIds(catalogEntry),
|
|
147
|
+
lifecycle: {
|
|
148
|
+
installed: declared || catalogued,
|
|
149
|
+
configured,
|
|
150
|
+
enabled: !tombstoned && declaration?.enabled !== false,
|
|
151
|
+
catalogued,
|
|
152
|
+
},
|
|
153
|
+
credential_refs: normalizeCredentialRefs(providerDeclarations),
|
|
154
|
+
references: {
|
|
155
|
+
harness_default: harnessDefault === id,
|
|
156
|
+
...(harnessDefault === id && normalizeAuthority(catalog?.harness_default_authority) ? { harness_default_authority: normalizeAuthority(catalog.harness_default_authority) } : {}),
|
|
157
|
+
worker_priority: findPriorityIndex(policy?.worker, 'priority', id),
|
|
158
|
+
worker_escalation: findPriorityIndex(policy?.worker, 'escalation_priority', id),
|
|
159
|
+
reviewer_priority: findPriorityIndex(policy?.reviewer ?? policy?.review, 'priority', id),
|
|
160
|
+
reviewer_escalation: findPriorityIndex(policy?.reviewer ?? policy?.review, 'escalation_priority', id),
|
|
161
|
+
active_jobs: active,
|
|
162
|
+
multimodal_refs: multimodal,
|
|
163
|
+
},
|
|
164
|
+
desired_state: tombstoned ? 'absent' : 'present',
|
|
165
|
+
activation: declared ? 'restart-required' : 'live',
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return { schema_version: 1, records, harness_default: catalog?.harness_default ?? null };
|
|
170
|
+
}
|
|
@@ -1,95 +1,100 @@
|
|
|
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
|
-
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
6
|
-
const REVISION_PATTERN = /^[a-f0-9]{64}$/;
|
|
7
|
-
const STATES = new Set([
|
|
8
|
-
'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
|
|
9
|
-
'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
function validId(value) {
|
|
13
|
-
return typeof value === 'string' && ID_PATTERN.test(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function validRevision(value) {
|
|
17
|
-
return typeof value === 'string' && REVISION_PATTERN.test(value);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
6
|
+
const REVISION_PATTERN = /^[a-f0-9]{64}$/;
|
|
7
|
+
const STATES = new Set([
|
|
8
|
+
'PLANNED', 'BLOCKED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING',
|
|
9
|
+
'VERIFIED', 'ROLLBACK_PENDING', 'ROLLED_BACK', 'FAILED',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function validId(value) {
|
|
13
|
+
return typeof value === 'string' && ID_PATTERN.test(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validRevision(value) {
|
|
17
|
+
return typeof value === 'string' && REVISION_PATTERN.test(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validTimestamp(value) {
|
|
21
|
+
return typeof value === 'string' && value.length >= 16 && value.length <= 64;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizedCredentialRefs(value) {
|
|
25
|
+
if (!Array.isArray(value)) return undefined;
|
|
26
|
+
const refs = value.map((ref) => {
|
|
27
|
+
if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return null;
|
|
28
|
+
const kind = typeof ref.kind === 'string' && ref.kind.trim() ? ref.kind.trim() : null;
|
|
29
|
+
const name = typeof ref.name_or_handle === 'string' && ref.name_or_handle.trim() ? ref.name_or_handle.trim() : null;
|
|
30
|
+
const ownership = typeof ref.ownership === 'string' && ref.ownership.trim() ? ref.ownership.trim() : null;
|
|
31
|
+
return kind && name && ownership ? { kind, name_or_handle: name, ownership } : null;
|
|
32
|
+
}).filter(Boolean).slice(0, 32);
|
|
33
|
+
return refs;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clone(value) {
|
|
37
|
+
return JSON.parse(JSON.stringify(value));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizedTransaction(value) {
|
|
41
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
42
|
+
if (!validId(value.provider_id) || typeof value.state !== 'string' || !STATES.has(value.state)) return null;
|
|
43
|
+
const refs = normalizedCredentialRefs(value.credential_refs);
|
|
44
|
+
return {
|
|
45
|
+
provider_id: value.provider_id,
|
|
46
|
+
state: value.state,
|
|
47
|
+
...(validTimestamp(value.updated_at) ? { updated_at: value.updated_at } : {}),
|
|
48
|
+
...(validRevision(value.expected_revision) ? { expected_revision: value.expected_revision } : {}),
|
|
49
|
+
...(refs === undefined ? {} : { credential_refs: refs }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Normalize persisted lifecycle metadata; unknown fields and secret values are discarded. */
|
|
54
|
+
export function normalizeProviderLifecycleState(input = {}) {
|
|
55
|
+
const out = { schema_version: 1, tombstones: {}, transactions: {}, last_verified_revision: {} };
|
|
56
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return out;
|
|
57
|
+
if (input.tombstones && typeof input.tombstones === 'object' && !Array.isArray(input.tombstones)) {
|
|
58
|
+
for (const [id, value] of Object.entries(input.tombstones)) if (validId(id) && value === 'absent') out.tombstones[id] = 'absent';
|
|
59
|
+
}
|
|
60
|
+
if (input.transactions && typeof input.transactions === 'object' && !Array.isArray(input.transactions)) {
|
|
61
|
+
for (const [transactionId, value] of Object.entries(input.transactions)) {
|
|
62
|
+
if (!validId(transactionId)) continue;
|
|
63
|
+
const transaction = normalizedTransaction(value);
|
|
64
|
+
if (transaction) out.transactions[transactionId] = transaction;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (input.last_verified_revision && typeof input.last_verified_revision === 'object' && !Array.isArray(input.last_verified_revision)) {
|
|
68
|
+
for (const [id, revision] of Object.entries(input.last_verified_revision)) if (validId(id) && validRevision(revision)) out.last_verified_revision[id] = revision;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireProviderId(providerId) {
|
|
74
|
+
if (!validId(providerId)) throw new TypeError('provider id must be a bounded identifier');
|
|
75
|
+
return providerId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function markProviderTombstone(state, providerId) {
|
|
79
|
+
const id = requireProviderId(providerId);
|
|
80
|
+
const out = normalizeProviderLifecycleState(state);
|
|
81
|
+
out.tombstones[id] = 'absent';
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function clearProviderTombstone(state, providerId) {
|
|
86
|
+
const id = requireProviderId(providerId);
|
|
87
|
+
const out = normalizeProviderLifecycleState(state);
|
|
88
|
+
delete out.tombstones[id];
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Record non-secret transaction metadata and credential reference names only. */
|
|
93
|
+
export function recordProviderTransaction(state, transaction = {}) {
|
|
94
|
+
const transactionId = requireProviderId(transaction.transaction_id ?? transaction.plan_id);
|
|
95
|
+
const normalized = normalizedTransaction(transaction);
|
|
96
|
+
if (!normalized) throw new TypeError('provider transaction metadata is invalid');
|
|
97
|
+
const out = normalizeProviderLifecycleState(state);
|
|
98
|
+
out.transactions[transactionId] = clone(normalized);
|
|
99
|
+
return out;
|
|
100
|
+
}
|