@upx-us/shield 0.2.14-beta → 0.2.16-beta

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/dist/index.js CHANGED
@@ -261,6 +261,11 @@ exports.default = {
261
261
  try {
262
262
  const entries = await fetchNewEntries(config);
263
263
  if (entries.length === 0) {
264
+ // Still commit cursors so initCursorsForDir positions
265
+ // (set to current file sizes) are persisted on disk.
266
+ // Without this, each poll re-initialises cursors to the
267
+ // NEW file size and silently skips events between polls.
268
+ commitCursors(config, []);
264
269
  state.consecutiveFailures = 0;
265
270
  state.lastPollAt = Date.now();
266
271
  persistState();
@@ -277,6 +282,24 @@ exports.default = {
277
282
  envelopes = envelopes.map(e => redactEvent(e));
278
283
  }
279
284
  const results = await sendEvents(envelopes, config);
285
+ // 403 needs_registration → self-halt cleanly (no point retrying)
286
+ const needsReg = results.some(r => r.needsRegistration);
287
+ if (needsReg) {
288
+ log.error('shield', 'Instance not registered on platform — Shield deactivated. Re-run: npx shield-setup');
289
+ state.running = false;
290
+ return;
291
+ }
292
+ // 202 pending_namespace → hold cursors, don't count as failure, use retry_after
293
+ const pending = results.find(r => r.pendingNamespace);
294
+ if (pending) {
295
+ const waitMs = pending.retryAfterMs ?? 300_000;
296
+ log.warn('shield', `Namespace allocation in progress — holding events, backing off ${Math.round(waitMs / 1000)}s`);
297
+ state.lastPollAt = Date.now();
298
+ persistState();
299
+ // Override next poll interval by sleeping here (schedulePoll will use normal backoff after)
300
+ await new Promise(r => setTimeout(r, waitMs));
301
+ return;
302
+ }
280
303
  const accepted = results.reduce((sum, r) => sum + (r.success ? r.eventCount : 0), 0);
281
304
  if (accepted > 0) {
282
305
  commitCursors(config, entries);
@@ -15,6 +15,11 @@ export interface SendResult {
15
15
  statusCode?: number;
16
16
  body?: string;
17
17
  eventCount: number;
18
+ /** True when Cloud Run returns 202 pending_namespace — hold cursors, backoff retry_after */
19
+ pendingNamespace?: boolean;
20
+ retryAfterMs?: number;
21
+ /** True when Cloud Run returns 403 needs_registration — plugin must self-halt */
22
+ needsRegistration?: boolean;
18
23
  }
19
24
  export declare function sendEvents(events: EnvelopeEvent[], config: Config): Promise<SendResult[]>;
20
25
  /**
@@ -115,7 +115,8 @@ async function sendEvents(events, config) {
115
115
  method: 'POST',
116
116
  headers: {
117
117
  'Content-Type': 'application/json',
118
- 'X-Shield-Instance-Id': instanceId,
118
+ 'X-Shield-Instance-Id': instanceId, // legacy name
119
+ 'X-Shield-Fingerprint': instanceId, // canonical name
119
120
  'X-Shield-Nonce': nonce,
120
121
  'X-Shield-Signature': signature,
121
122
  'X-Shield-Version': version_1.VERSION,
@@ -125,10 +126,37 @@ async function sendEvents(events, config) {
125
126
  });
126
127
  const data = await res.text();
127
128
  log.info('sender', `Batch ${batchNum}: ${batch.length} events (HTTP ${res.status})`);
128
- if (res.ok) {
129
+ if (res.ok && res.status === 200) {
129
130
  results.push({ success: true, statusCode: res.status, body: data, eventCount: batch.length });
130
131
  break;
131
132
  }
133
+ // 202 pending_namespace — namespace not yet allocated by SIEM.
134
+ // Events must NOT be committed; plugin should hold and retry after retry_after.
135
+ if (res.status === 202) {
136
+ let retryAfterMs = 300_000; // default 5 min
137
+ try {
138
+ retryAfterMs = (JSON.parse(data).retry_after ?? 300) * 1000;
139
+ }
140
+ catch { }
141
+ log.warn('sender', `Batch ${batchNum} — namespace pending (202). Holding events, retry in ${retryAfterMs / 1000}s`);
142
+ results.push({ success: false, statusCode: 202, body: data, eventCount: batch.length,
143
+ pendingNamespace: true, retryAfterMs });
144
+ break;
145
+ }
146
+ // 403 needs_registration — instance unknown or inactive, plugin must self-halt.
147
+ if (res.status === 403) {
148
+ let needsReg = false;
149
+ try {
150
+ needsReg = JSON.parse(data).needs_registration === true;
151
+ }
152
+ catch { }
153
+ if (needsReg) {
154
+ log.error('sender', `Batch ${batchNum} — instance not registered (403). Shield deactivated — re-run wizard.`);
155
+ results.push({ success: false, statusCode: 403, body: data, eventCount: batch.length,
156
+ needsRegistration: true });
157
+ break;
158
+ }
159
+ }
132
160
  if (res.status >= 500 && attempt === 0) {
133
161
  log.warn('sender', `Batch ${batchNum} attempt ${attempt + 1} — HTTP ${res.status}, retrying...`);
134
162
  continue;
@@ -170,7 +198,8 @@ async function reportInstance(payload, credentials) {
170
198
  method: 'PUT',
171
199
  headers: {
172
200
  'Content-Type': 'application/json',
173
- 'X-Shield-Instance-Id': instanceId,
201
+ 'X-Shield-Instance-Id': instanceId, // legacy
202
+ 'X-Shield-Fingerprint': instanceId, // canonical
174
203
  'X-Shield-Nonce': nonce,
175
204
  'X-Shield-Signature': signature,
176
205
  'X-Shield-Version': version_1.VERSION,
@@ -2,7 +2,7 @@
2
2
  "id": "shield",
3
3
  "name": "OpenClaw Shield",
4
4
  "description": "Real-time security monitoring — streams enriched, redacted security events to the Shield detection platform.",
5
- "version": "0.2.14-beta",
5
+ "version": "0.2.16-beta",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upx-us/shield",
3
- "version": "0.2.14-beta",
3
+ "version": "0.2.16-beta",
4
4
  "description": "Security monitoring plugin for OpenClaw agents — streams enriched security events to the Shield detection platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",