blun-king-cli 9.1.66 → 9.1.68

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.
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+
5
+ const EVENT_FIELDS = new Set([
6
+ 'event_id',
7
+ 'run_id',
8
+ 'project_id',
9
+ 'actor',
10
+ 'event_type',
11
+ 'occurred_at',
12
+ 'payload',
13
+ 'previous_event_hash',
14
+ ]);
15
+ const ACTOR_FIELDS = new Set(['type', 'id', 'session_id']);
16
+ const ACTOR_TYPES = new Set(['agent', 'user', 'system']);
17
+ const SECRET_FIELDS = new Set([
18
+ 'access_token',
19
+ 'api_key',
20
+ 'authorization',
21
+ 'bot_token',
22
+ 'client_secret',
23
+ 'cookie',
24
+ 'credential',
25
+ 'password',
26
+ 'passwd',
27
+ 'private_key',
28
+ 'refresh_token',
29
+ 'secret',
30
+ 'session_string',
31
+ 'token',
32
+ ]);
33
+ const HASH_PATTERN = /^sha256:[a-f0-9]{64}$/u;
34
+
35
+ function uvfError(code, message) {
36
+ const error = new Error(message);
37
+ error.code = code;
38
+ return error;
39
+ }
40
+
41
+ function isRecord(value) {
42
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
43
+ }
44
+
45
+ function requireText(value, field) {
46
+ if (typeof value !== 'string' || value.trim().length === 0) {
47
+ throw uvfError('UVF_INVALID_EVENT', `${field} must be a non-empty string`);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function assertKnownFields(value, allowed, label) {
53
+ const unknown = Object.keys(value).filter((field) => !allowed.has(field));
54
+ if (unknown.length > 0) {
55
+ throw uvfError('UVF_INVALID_EVENT', `unknown ${label} fields: ${unknown.sort().join(', ')}`);
56
+ }
57
+ }
58
+
59
+ function assertNoSecretFields(value, path = 'payload', seen = new Set()) {
60
+ if (value === null || typeof value !== 'object') return;
61
+ if (seen.has(value)) throw uvfError('UVF_INVALID_EVENT', `${path} contains a cycle`);
62
+ seen.add(value);
63
+ if (Array.isArray(value)) {
64
+ value.forEach((entry, index) => assertNoSecretFields(entry, `${path}[${index}]`, seen));
65
+ } else {
66
+ for (const [key, entry] of Object.entries(value)) {
67
+ const normalizedKey = key.toLowerCase().replaceAll('-', '_');
68
+ const secretLike = SECRET_FIELDS.has(normalizedKey)
69
+ || normalizedKey.endsWith('_token')
70
+ || /(?:^|_)(?:password|passwd|secret|authorization|cookie|credential)(?:_|$)/u.test(normalizedKey)
71
+ || /(?:^|_)(?:api_key|private_key|session_string)(?:_|$)/u.test(normalizedKey);
72
+ if (secretLike) {
73
+ throw uvfError('UVF_SECRET_FIELD_REJECTED', `${path}.${key} is not allowed in audit data`);
74
+ }
75
+ assertNoSecretFields(entry, `${path}.${key}`, seen);
76
+ }
77
+ }
78
+ seen.delete(value);
79
+ }
80
+
81
+ function canonicalize(value, path = 'event', seen = new Set()) {
82
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
83
+ if (typeof value === 'number') {
84
+ if (!Number.isFinite(value)) throw uvfError('UVF_INVALID_EVENT', `${path} contains a non-finite number`);
85
+ return value;
86
+ }
87
+ if (typeof value !== 'object') {
88
+ throw uvfError('UVF_INVALID_EVENT', `${path} contains a non-JSON value`);
89
+ }
90
+ if (seen.has(value)) throw uvfError('UVF_INVALID_EVENT', `${path} contains a cycle`);
91
+ seen.add(value);
92
+ let result;
93
+ if (Array.isArray(value)) {
94
+ result = value.map((entry, index) => canonicalize(entry, `${path}[${index}]`, seen));
95
+ } else {
96
+ result = {};
97
+ for (const key of Object.keys(value).sort()) {
98
+ result[key] = canonicalize(value[key], `${path}.${key}`, seen);
99
+ }
100
+ }
101
+ seen.delete(value);
102
+ return result;
103
+ }
104
+
105
+ function eventDigest(eventWithoutHash) {
106
+ const canonical = JSON.stringify(canonicalize(eventWithoutHash));
107
+ return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
108
+ }
109
+
110
+ function validateEventInput(input) {
111
+ if (!isRecord(input)) throw uvfError('UVF_INVALID_EVENT', 'event must be an object');
112
+ assertKnownFields(input, EVENT_FIELDS, 'event');
113
+ requireText(input.event_id, 'event_id');
114
+ requireText(input.run_id, 'run_id');
115
+ requireText(input.project_id, 'project_id');
116
+ requireText(input.event_type, 'event_type');
117
+ if (!isRecord(input.actor)) throw uvfError('UVF_INVALID_EVENT', 'actor must be an object');
118
+ assertKnownFields(input.actor, ACTOR_FIELDS, 'actor');
119
+ if (!ACTOR_TYPES.has(input.actor.type)) {
120
+ throw uvfError('UVF_INVALID_EVENT', `unsupported actor type: ${String(input.actor.type)}`);
121
+ }
122
+ requireText(input.actor.id, 'actor.id');
123
+ requireText(input.actor.session_id, 'actor.session_id');
124
+ requireText(input.occurred_at, 'occurred_at');
125
+ const occurredAt = new Date(input.occurred_at);
126
+ if (Number.isNaN(occurredAt.getTime()) || occurredAt.toISOString() !== input.occurred_at) {
127
+ throw uvfError('UVF_INVALID_EVENT', 'occurred_at must be a canonical ISO timestamp');
128
+ }
129
+ if (!isRecord(input.payload)) throw uvfError('UVF_INVALID_EVENT', 'payload must be an object');
130
+ assertNoSecretFields(input.payload);
131
+ if (input.previous_event_hash !== undefined
132
+ && input.previous_event_hash !== null
133
+ && (typeof input.previous_event_hash !== 'string'
134
+ || !HASH_PATTERN.test(input.previous_event_hash))) {
135
+ throw uvfError('UVF_INVALID_EVENT', 'previous_event_hash must be a sha256 hash or null');
136
+ }
137
+ }
138
+
139
+ function deepFreeze(value) {
140
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
141
+ Object.freeze(value);
142
+ for (const child of Object.values(value)) deepFreeze(child);
143
+ }
144
+ return value;
145
+ }
146
+
147
+ function createExecutionEvent(input) {
148
+ validateEventInput(input);
149
+ const event = {
150
+ event_id: input.event_id,
151
+ run_id: input.run_id,
152
+ project_id: input.project_id,
153
+ actor: canonicalize(input.actor),
154
+ event_type: input.event_type,
155
+ occurred_at: input.occurred_at,
156
+ payload: canonicalize(input.payload),
157
+ previous_event_hash: input.previous_event_hash ?? null,
158
+ };
159
+ return deepFreeze({ ...event, event_hash: eventDigest(event) });
160
+ }
161
+
162
+ function verifyEventChain(events) {
163
+ if (!Array.isArray(events)) return { ok: false, index: -1, reason: 'invalid_chain' };
164
+ let previousHash = null;
165
+ for (let index = 0; index < events.length; index += 1) {
166
+ const event = events[index];
167
+ if (!isRecord(event) || !HASH_PATTERN.test(event.event_hash || '')) {
168
+ return { ok: false, index, reason: 'invalid_event' };
169
+ }
170
+ if ((event.previous_event_hash ?? null) !== previousHash) {
171
+ return { ok: false, index, reason: 'previous_event_hash_mismatch' };
172
+ }
173
+ const { event_hash: eventHash, ...input } = event;
174
+ let rebuilt;
175
+ try {
176
+ rebuilt = createExecutionEvent(input);
177
+ } catch {
178
+ return { ok: false, index, reason: 'invalid_event' };
179
+ }
180
+ if (rebuilt.event_hash !== eventHash) {
181
+ return { ok: false, index, reason: 'event_hash_mismatch' };
182
+ }
183
+ previousHash = eventHash;
184
+ }
185
+ return { ok: true };
186
+ }
187
+
188
+ module.exports = {
189
+ createExecutionEvent,
190
+ verifyEventChain,
191
+ };
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const RUN_STATES = Object.freeze([
4
+ 'discovered',
5
+ 'profiled',
6
+ 'configured',
7
+ 'baseline_measured',
8
+ 'observing',
9
+ 'evidence_ready',
10
+ 'proposal_created',
11
+ 'selected_by_strategic_authority',
12
+ 'approved_for_local_execution',
13
+ 'executing_local',
14
+ 'verifying',
15
+ 'blocked',
16
+ 'aborted',
17
+ 'no_action',
18
+ 'locally_verified',
19
+ 'completed_local',
20
+ 'waiting_approval',
21
+ 'denied',
22
+ 'expired',
23
+ 'approved_exact_state',
24
+ 'deploying',
25
+ 'post_deploy_observation',
26
+ 'attribution_pending',
27
+ 'completed',
28
+ 'rolled_back',
29
+ 'inconclusive',
30
+ ]);
31
+
32
+ const TRANSITIONS = Object.freeze({
33
+ discovered: Object.freeze(['profiled']),
34
+ profiled: Object.freeze(['configured']),
35
+ configured: Object.freeze(['baseline_measured']),
36
+ baseline_measured: Object.freeze(['observing']),
37
+ observing: Object.freeze(['evidence_ready', 'no_action', 'blocked', 'aborted']),
38
+ evidence_ready: Object.freeze(['proposal_created', 'no_action', 'blocked', 'aborted']),
39
+ proposal_created: Object.freeze(['selected_by_strategic_authority', 'no_action', 'blocked', 'aborted']),
40
+ selected_by_strategic_authority: Object.freeze(['approved_for_local_execution', 'no_action', 'blocked', 'aborted']),
41
+ approved_for_local_execution: Object.freeze(['executing_local', 'blocked', 'aborted']),
42
+ executing_local: Object.freeze(['verifying', 'blocked', 'aborted']),
43
+ verifying: Object.freeze(['locally_verified', 'blocked', 'aborted', 'no_action']),
44
+ locally_verified: Object.freeze(['completed_local', 'waiting_approval', 'verifying']),
45
+ waiting_approval: Object.freeze(['approved_exact_state', 'denied', 'expired', 'aborted']),
46
+ approved_exact_state: Object.freeze(['deploying', 'verifying', 'aborted']),
47
+ deploying: Object.freeze(['post_deploy_observation', 'blocked', 'aborted']),
48
+ post_deploy_observation: Object.freeze(['attribution_pending', 'blocked', 'aborted']),
49
+ attribution_pending: Object.freeze(['completed', 'rolled_back', 'inconclusive', 'blocked', 'aborted']),
50
+ });
51
+
52
+ const TERMINAL_STATE_VALUES = Object.freeze([
53
+ 'blocked',
54
+ 'aborted',
55
+ 'no_action',
56
+ 'completed_local',
57
+ 'denied',
58
+ 'expired',
59
+ 'completed',
60
+ 'rolled_back',
61
+ 'inconclusive',
62
+ ]);
63
+ const TERMINAL_STATE_LOOKUP = new Set(TERMINAL_STATE_VALUES);
64
+ const TERMINAL_STATES = Object.freeze({
65
+ get size() {
66
+ return TERMINAL_STATE_VALUES.length;
67
+ },
68
+ has(state) {
69
+ return TERMINAL_STATE_LOOKUP.has(state);
70
+ },
71
+ values() {
72
+ return TERMINAL_STATE_VALUES.values();
73
+ },
74
+ [Symbol.iterator]() {
75
+ return TERMINAL_STATE_VALUES[Symbol.iterator]();
76
+ },
77
+ });
78
+
79
+ const REQUEST_FIELDS = new Set([
80
+ 'event_id',
81
+ 'expected_version',
82
+ 'from_state',
83
+ 'to_state',
84
+ 'evidence',
85
+ ]);
86
+
87
+ function uvfError(code, message) {
88
+ const error = new Error(message);
89
+ error.code = code;
90
+ return error;
91
+ }
92
+
93
+ function isRecord(value) {
94
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
95
+ }
96
+
97
+ function requireText(value, field) {
98
+ if (typeof value !== 'string' || value.trim().length === 0) {
99
+ throw uvfError('UVF_INVALID_TRANSITION_REQUEST', `${field} must be a non-empty string`);
100
+ }
101
+ return value;
102
+ }
103
+
104
+ function validateRun(run) {
105
+ if (!isRecord(run)) throw uvfError('UVF_INVALID_RUN', 'run must be an object');
106
+ requireText(run.run_id, 'run_id');
107
+ if (!RUN_STATES.includes(run.state)) {
108
+ throw uvfError('UVF_INVALID_RUN_STATE', `unknown run state: ${String(run.state)}`);
109
+ }
110
+ if (!Number.isSafeInteger(run.version) || run.version < 0) {
111
+ throw uvfError('UVF_INVALID_RUN_VERSION', 'run version must be a non-negative integer');
112
+ }
113
+ }
114
+
115
+ function validateRequest(request) {
116
+ if (!isRecord(request)) {
117
+ throw uvfError('UVF_INVALID_TRANSITION_REQUEST', 'transition request must be an object');
118
+ }
119
+ const unknownFields = Object.keys(request).filter((field) => !REQUEST_FIELDS.has(field));
120
+ if (unknownFields.length > 0) {
121
+ throw uvfError(
122
+ 'UVF_INVALID_TRANSITION_REQUEST',
123
+ `unknown transition fields: ${unknownFields.sort().join(', ')}`,
124
+ );
125
+ }
126
+ requireText(request.event_id, 'event_id');
127
+ requireText(request.from_state, 'from_state');
128
+ requireText(request.to_state, 'to_state');
129
+ if (!Number.isSafeInteger(request.expected_version) || request.expected_version < 0) {
130
+ throw uvfError('UVF_INVALID_TRANSITION_REQUEST', 'expected_version must be a non-negative integer');
131
+ }
132
+ if (request.evidence !== undefined && !isRecord(request.evidence)) {
133
+ throw uvfError('UVF_INVALID_TRANSITION_REQUEST', 'evidence must be an object');
134
+ }
135
+ }
136
+
137
+ function requireEvidence(request, field, code) {
138
+ if (typeof request.evidence?.[field] !== 'string'
139
+ || request.evidence[field].trim().length === 0) {
140
+ throw uvfError(code, `${request.to_state} requires ${field}`);
141
+ }
142
+ }
143
+
144
+ function transitionRun(run, request) {
145
+ validateRun(run);
146
+ validateRequest(request);
147
+
148
+ if (request.expected_version !== run.version) {
149
+ throw uvfError(
150
+ 'UVF_VERSION_CONFLICT',
151
+ `expected version ${request.expected_version}, current version ${run.version}`,
152
+ );
153
+ }
154
+ if (request.from_state !== run.state) {
155
+ throw uvfError(
156
+ 'UVF_STATE_CONFLICT',
157
+ `expected state ${request.from_state}, current state ${run.state}`,
158
+ );
159
+ }
160
+ if (!RUN_STATES.includes(request.to_state)) {
161
+ throw uvfError('UVF_INVALID_RUN_STATE', `unknown target state: ${request.to_state}`);
162
+ }
163
+ const allowed = TRANSITIONS[run.state] || [];
164
+ if (!allowed.includes(request.to_state)) {
165
+ throw uvfError(
166
+ 'UVF_INVALID_TRANSITION',
167
+ `transition ${run.state} -> ${request.to_state} is not allowed`,
168
+ );
169
+ }
170
+ if (request.to_state === 'completed') {
171
+ requireEvidence(request, 'final_evidence_id', 'UVF_FINAL_EVIDENCE_REQUIRED');
172
+ }
173
+ if (request.to_state === 'rolled_back') {
174
+ requireEvidence(request, 'rollback_evidence_id', 'UVF_ROLLBACK_EVIDENCE_REQUIRED');
175
+ }
176
+
177
+ return Object.freeze({
178
+ ...run,
179
+ state: request.to_state,
180
+ version: run.version + 1,
181
+ last_event_id: request.event_id,
182
+ });
183
+ }
184
+
185
+ module.exports = {
186
+ RUN_STATES,
187
+ TERMINAL_STATES,
188
+ TRANSITIONS,
189
+ transitionRun,
190
+ };