@astrasyncai/verification-gateway 5.10.0 → 5.12.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.
Files changed (106) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/express.d.mts +1 -1
  3. package/dist/adapters/express.d.ts +1 -1
  4. package/dist/adapters/express.js +359 -30
  5. package/dist/adapters/express.js.map +1 -1
  6. package/dist/adapters/express.mjs +359 -30
  7. package/dist/adapters/express.mjs.map +1 -1
  8. package/dist/adapters/mcp.d.mts +5 -1
  9. package/dist/adapters/mcp.d.ts +5 -1
  10. package/dist/adapters/mcp.js +168 -41
  11. package/dist/adapters/mcp.js.map +1 -1
  12. package/dist/adapters/mcp.mjs +168 -41
  13. package/dist/adapters/mcp.mjs.map +1 -1
  14. package/dist/adapters/nextjs.d.mts +1 -1
  15. package/dist/adapters/nextjs.d.ts +1 -1
  16. package/dist/adapters/nextjs.js +352 -13
  17. package/dist/adapters/nextjs.js.map +1 -1
  18. package/dist/adapters/nextjs.mjs +352 -13
  19. package/dist/adapters/nextjs.mjs.map +1 -1
  20. package/dist/adapters/sdk.d.mts +9 -1
  21. package/dist/adapters/sdk.d.ts +9 -1
  22. package/dist/adapters/sdk.js +100 -8
  23. package/dist/adapters/sdk.js.map +1 -1
  24. package/dist/adapters/sdk.mjs +100 -8
  25. package/dist/adapters/sdk.mjs.map +1 -1
  26. package/dist/agent/index.js +1 -1
  27. package/dist/agent/index.js.map +1 -1
  28. package/dist/agent/index.mjs +1 -1
  29. package/dist/agent/index.mjs.map +1 -1
  30. package/dist/bin/astrasync-claude-hook.js +805 -16
  31. package/dist/bin/astrasync-codex-hook.js +805 -16
  32. package/dist/bin/astrasync-guard.js +894 -71
  33. package/dist/bin/astrasync.js +951 -74
  34. package/dist/browser/background.js +284 -7
  35. package/dist/browser/background.js.map +1 -1
  36. package/dist/browser/background.mjs +284 -7
  37. package/dist/browser/background.mjs.map +1 -1
  38. package/dist/cli/index.d.mts +8 -8
  39. package/dist/cli/index.d.ts +8 -8
  40. package/dist/cli/index.js +22 -64
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/cli/index.mjs +22 -64
  43. package/dist/cli/index.mjs.map +1 -1
  44. package/dist/codex/index.d.mts +5 -0
  45. package/dist/codex/index.d.ts +5 -0
  46. package/dist/codex/index.js +821 -24
  47. package/dist/codex/index.js.map +1 -1
  48. package/dist/codex/index.mjs +823 -26
  49. package/dist/codex/index.mjs.map +1 -1
  50. package/dist/cursor/extension.d.mts +9 -2
  51. package/dist/cursor/extension.d.ts +9 -2
  52. package/dist/cursor/extension.js +787 -11
  53. package/dist/cursor/extension.js.map +1 -1
  54. package/dist/cursor/extension.mjs +789 -11
  55. package/dist/cursor/extension.mjs.map +1 -1
  56. package/dist/edge-config.d.mts +1 -1
  57. package/dist/edge-config.d.ts +1 -1
  58. package/dist/edge-config.js +1 -1
  59. package/dist/edge-config.js.map +1 -1
  60. package/dist/edge-config.mjs +1 -1
  61. package/dist/edge-config.mjs.map +1 -1
  62. package/dist/edge-core/index.d.mts +128 -13
  63. package/dist/edge-core/index.d.ts +128 -13
  64. package/dist/edge-core/index.js +337 -55
  65. package/dist/edge-core/index.js.map +1 -1
  66. package/dist/edge-core/index.mjs +334 -55
  67. package/dist/edge-core/index.mjs.map +1 -1
  68. package/dist/enrolment-store-8DyaIOLe.d.mts +26 -0
  69. package/dist/enrolment-store-8DyaIOLe.d.ts +26 -0
  70. package/dist/gateway/gateway.js +284 -7
  71. package/dist/gateway/gateway.js.map +1 -1
  72. package/dist/gateway/gateway.mjs +284 -7
  73. package/dist/gateway/gateway.mjs.map +1 -1
  74. package/dist/git-trigger/git-hooks.d.mts +1 -1
  75. package/dist/git-trigger/git-hooks.d.ts +1 -1
  76. package/dist/index.d.mts +215 -5
  77. package/dist/index.d.ts +215 -5
  78. package/dist/index.js +3756 -80
  79. package/dist/index.js.map +1 -1
  80. package/dist/index.mjs +3748 -80
  81. package/dist/index.mjs.map +1 -1
  82. package/dist/platform-signatures.js +4 -1
  83. package/dist/platform-signatures.js.map +1 -1
  84. package/dist/platform-signatures.mjs +4 -1
  85. package/dist/platform-signatures.mjs.map +1 -1
  86. package/dist/registration/index.js +1 -1
  87. package/dist/registration/index.js.map +1 -1
  88. package/dist/registration/index.mjs +1 -1
  89. package/dist/registration/index.mjs.map +1 -1
  90. package/dist/transport/index.js +1 -1
  91. package/dist/transport/index.js.map +1 -1
  92. package/dist/transport/index.mjs +1 -1
  93. package/dist/transport/index.mjs.map +1 -1
  94. package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
  95. package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
  96. package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
  97. package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
  98. package/dist/ui/index.d.mts +1 -1
  99. package/dist/ui/index.d.ts +1 -1
  100. package/dist/verify.d.mts +35 -4
  101. package/dist/verify.d.ts +35 -4
  102. package/dist/verify.js +130 -16
  103. package/dist/verify.js.map +1 -1
  104. package/dist/verify.mjs +129 -16
  105. package/dist/verify.mjs.map +1 -1
  106. package/package.json +1 -1
@@ -36,7 +36,7 @@ var SDK_VERSION;
36
36
  var init_version = __esm({
37
37
  "src/version.ts"() {
38
38
  "use strict";
39
- SDK_VERSION = "5.10.0";
39
+ SDK_VERSION = "5.12.0";
40
40
  }
41
41
  });
42
42
 
@@ -4095,7 +4095,6 @@ var init_upgrade = __esm({
4095
4095
  "src/cli/upgrade.ts"() {
4096
4096
  "use strict";
4097
4097
  init_http();
4098
- init_pdlss_parser();
4099
4098
  UpgradeFlow = class {
4100
4099
  constructor(config, callbacks, trigger) {
4101
4100
  this.config = config;
@@ -4155,27 +4154,23 @@ var init_upgrade = __esm({
4155
4154
  */
4156
4155
  async continueUpgrade(apiKey, kydId) {
4157
4156
  const { print, prompt } = this.callbacks;
4158
- print("\n[3/5] Migrate local policy to cloud");
4159
- print(` Policy: ${this.config.policyPath}`);
4160
- print(` Name: ${this.config.policy.name}`);
4161
- print(` Rules: ${this.config.policy.purposes.length} purpose rules`);
4162
- if (this.config.policy.scope) {
4163
- const scope = this.config.policy.scope;
4164
- const domains = [
4165
- ...(scope.allowedDomains || []).length ? [`${scope.allowedDomains.length} allowed`] : [],
4166
- ...(scope.blockedDomains || []).length ? [`${scope.blockedDomains.length} blocked`] : []
4167
- ];
4168
- if (domains.length) print(` Scope: ${domains.join(", ")} domains`);
4169
- }
4170
- print("");
4171
- print(" Uploading YAML -> Cloud PDLSS boundary...");
4172
- const uploadResult = await this.uploadPolicy(apiKey);
4173
- if (!uploadResult.success) {
4174
- print(` Upload failed: ${uploadResult.error}`);
4175
- return uploadResult;
4176
- }
4177
- print(" Policy uploaded. Same rules, now with trust scoring and audit.");
4178
- print(` Agent registered: ${uploadResult.agentId}`);
4157
+ print("\n[3/5] Link your agent");
4158
+ print(
4159
+ ` Local policy ${this.config.policyPath} stays on this machine (${this.config.policy.purposes.length} purpose rules).`
4160
+ );
4161
+ print(" Register the agent in the AstraSync dashboard (or with `astrasync register`) and");
4162
+ print(" paste its ASTRA- id so synced decisions are attributed to it.\n");
4163
+ const agentInput = (await prompt(" Agent ASTRA- id (Enter to skip): ")).trim();
4164
+ let agentId;
4165
+ if (agentInput) {
4166
+ if (!/^ASTRA-[A-Za-z0-9_-]+$/.test(agentInput)) {
4167
+ return { success: false, error: `Not an ASTRA- agent id: ${agentInput}` };
4168
+ }
4169
+ agentId = agentInput;
4170
+ print(` Agent: ${agentId}`);
4171
+ } else {
4172
+ print(" No agent linked \u2014 decisions sync under your API key only.");
4173
+ }
4179
4174
  if (kydId) {
4180
4175
  print(` Developer: ${kydId}`);
4181
4176
  }
@@ -4186,23 +4181,23 @@ var init_upgrade = __esm({
4186
4181
  const mode = modeInput.trim().toLowerCase() === "online" ? "online" : "hybrid";
4187
4182
  print(`
4188
4183
  [5/5] Activating ${mode} verification`);
4189
- await this.writeLocalConfig(apiKey, uploadResult.agentId, mode, kydId);
4184
+ await this.writeLocalConfig(apiKey, agentId, mode, kydId);
4190
4185
  print(` Config written to ${this.configDir}/config`);
4191
4186
  print(` Mode: ${mode}`);
4192
4187
  print(" Done!\n");
4193
- print(` Your agent (${uploadResult.agentId}) is verified on the AstraSync network.`);
4188
+ if (agentId) print(` Your agent (${agentId}) is verified on the AstraSync network.`);
4194
4189
  if (mode === "hybrid") {
4195
4190
  print(" Local evaluation active for speed. Cloud sync for audit and trust scoring.");
4196
4191
  } else {
4197
4192
  print(" Trust scoring, audit trails, and blockchain recording are active.");
4198
4193
  }
4199
4194
  print(`
4200
- Dashboard: ${this.platformUrl}/agents/${uploadResult.agentId}
4195
+ Dashboard: ${this.platformUrl}/agents${agentId ? `/${agentId}` : ""}
4201
4196
  `);
4202
4197
  return {
4203
4198
  success: true,
4204
4199
  apiKey,
4205
- agentId: uploadResult.agentId,
4200
+ agentId,
4206
4201
  kydId,
4207
4202
  mode
4208
4203
  };
@@ -4255,44 +4250,6 @@ var init_upgrade = __esm({
4255
4250
  return { valid: true, error: `Could not reach platform (proceeding anyway)` };
4256
4251
  }
4257
4252
  }
4258
- /**
4259
- * Upload local policy to the AstraSync cloud.
4260
- * YAML -> cloud PDLSS boundary, 1:1 mapping, no lossy transformation.
4261
- */
4262
- async uploadPolicy(apiKey) {
4263
- const policyYaml = toYaml(this.config.policy);
4264
- try {
4265
- const response = await sdkFetch(`${this.platformUrl}/api/v1/agents/policies/upload`, {
4266
- method: "POST",
4267
- headers: {
4268
- "Content-Type": "application/json",
4269
- Authorization: `Bearer ${apiKey}`
4270
- },
4271
- body: JSON.stringify({
4272
- policy: policyYaml,
4273
- policyObject: this.config.policy,
4274
- format: "yaml",
4275
- source: "local-guard-upgrade",
4276
- name: this.config.policy.name
4277
- })
4278
- });
4279
- if (!response.ok) {
4280
- const body = await response.text();
4281
- return { success: false, error: `Upload failed (${response.status}): ${body}` };
4282
- }
4283
- const data = await response.json();
4284
- return {
4285
- success: true,
4286
- agentId: data.agentId || "ASTRA-PENDING",
4287
- kydId: data.kydId
4288
- };
4289
- } catch (err) {
4290
- return {
4291
- success: false,
4292
- error: `Network error: ${err instanceof Error ? err.message : String(err)}`
4293
- };
4294
- }
4295
- }
4296
4253
  /**
4297
4254
  * Write the config file for the chosen mode.
4298
4255
  */
@@ -5100,6 +5057,20 @@ var init_well_known = __esm({
5100
5057
  }
5101
5058
  });
5102
5059
 
5060
+ // src/failure-posture.ts
5061
+ var REJECTED_IDENTITY_DIMENSIONS, UNKNOWN_AGENT_GUIDANCE;
5062
+ var init_failure_posture = __esm({
5063
+ "src/failure-posture.ts"() {
5064
+ "use strict";
5065
+ REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
5066
+ "identity.invalid_agent_id",
5067
+ "identity.unknown_agent",
5068
+ "agent.lookup"
5069
+ ]);
5070
+ UNKNOWN_AGENT_GUIDANCE = "Register your agent with AstraSync (or, inside an MCP host, add the AstraSync MCP connector for keyless, human-approved registration), then present the canonical ASTRA-* id issued after owner approval.";
5071
+ }
5072
+ });
5073
+
5103
5074
  // src/verify.ts
5104
5075
  async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
5105
5076
  initCheckPerformed = true;
@@ -5298,11 +5269,43 @@ function createGuidanceResponse(_config, reason, options = {}) {
5298
5269
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
5299
5270
  // deterministic and intentionally NOT marked retryable.
5300
5271
  ...isApiError && { retryable: true },
5272
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
5273
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
5301
5274
  correlationId: options.correlationId,
5302
5275
  attemptId: options.attemptId,
5303
5276
  verifiedAt: /* @__PURE__ */ new Date()
5304
5277
  };
5305
5278
  }
5279
+ async function readJsonSafe(response) {
5280
+ try {
5281
+ const parsed = await response.json();
5282
+ return parsed && typeof parsed === "object" ? parsed : {};
5283
+ } catch {
5284
+ return {};
5285
+ }
5286
+ }
5287
+ function rejectedIdentityGuidance(failures) {
5288
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
5289
+ if (!f) return void 0;
5290
+ if (f.dimension === "agent.lookup") {
5291
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
5292
+ }
5293
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
5294
+ }
5295
+ function parseRetryAfterMs(response) {
5296
+ const raw = response.headers?.get?.("retry-after");
5297
+ if (!raw) return void 0;
5298
+ const trimmed = raw.trim();
5299
+ let ms;
5300
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
5301
+ ms = Number(trimmed) * 1e3;
5302
+ } else {
5303
+ const at = Date.parse(trimmed);
5304
+ if (Number.isNaN(at)) return void 0;
5305
+ ms = at - Date.now();
5306
+ }
5307
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
5308
+ }
5306
5309
  async function callVerifyAccessAPI(config, request2) {
5307
5310
  const { credentials, ...requestData } = request2;
5308
5311
  const body = {
@@ -5359,7 +5362,8 @@ async function callVerifyAccessAPI(config, request2) {
5359
5362
  body: JSON.stringify(body),
5360
5363
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
5361
5364
  });
5362
- const data = await response.json();
5365
+ const data = await readJsonSafe(response);
5366
+ const retryAfterMs = parseRetryAfterMs(response);
5363
5367
  if (response.status === 410) {
5364
5368
  return {
5365
5369
  success: true,
@@ -5397,6 +5401,23 @@ async function callVerifyAccessAPI(config, request2) {
5397
5401
  }
5398
5402
  };
5399
5403
  }
5404
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
5405
+ return {
5406
+ success: true,
5407
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
5408
+ access: {
5409
+ allowed: false,
5410
+ reason: "unknown_agent",
5411
+ failures: [
5412
+ {
5413
+ dimension: "identity.unknown_agent",
5414
+ message: "The presented agent identifier is not registered with AstraSync.",
5415
+ guidance: UNKNOWN_AGENT_GUIDANCE
5416
+ }
5417
+ ]
5418
+ }
5419
+ };
5420
+ }
5400
5421
  if (!response.ok) {
5401
5422
  if (response.status === 401) {
5402
5423
  return {
@@ -5411,9 +5432,11 @@ async function callVerifyAccessAPI(config, request2) {
5411
5432
  }
5412
5433
  return {
5413
5434
  success: false,
5414
- error: data.message || data.error || `API returned ${response.status}`,
5435
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
5415
5436
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
5416
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
5437
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
5438
+ httpStatus: response.status,
5439
+ ...retryAfterMs !== void 0 && { retryAfterMs }
5417
5440
  };
5418
5441
  }
5419
5442
  return data;
@@ -5485,12 +5508,14 @@ async function verify(config, request2, options) {
5485
5508
  correlationId: apiResponse.correlationId,
5486
5509
  attemptId: apiResponse.attemptId,
5487
5510
  remediation: apiResponse.remediation,
5511
+ httpStatus: apiResponse.httpStatus,
5512
+ retryAfterMs: apiResponse.retryAfterMs,
5488
5513
  urls
5489
5514
  });
5490
5515
  }
5491
5516
  if (!apiResponse.access?.allowed) {
5492
5517
  const aggregatedFailures = apiResponse.access?.failures;
5493
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
5518
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
5494
5519
  const result2 = {
5495
5520
  identityVerified: idVerifiedFromBackend,
5496
5521
  policyAllowed: false,
@@ -5502,7 +5527,10 @@ async function verify(config, request2, options) {
5502
5527
  requiresStepUp: apiResponse.access?.requiresStepUp,
5503
5528
  requiresApproval: apiResponse.access?.requiresApproval,
5504
5529
  guidance: {
5505
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
5530
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
5531
+ // actionable registration guidance on its failure — surface it
5532
+ // instead of the bare machine reason ('unknown_agent').
5533
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
5506
5534
  registrationUrl: urls?.registrationUrl ?? "",
5507
5535
  documentationUrl: urls?.documentationUrl ?? "",
5508
5536
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -5602,7 +5630,7 @@ async function verify(config, request2, options) {
5602
5630
  registrationUrl: urls?.registrationUrl ?? "",
5603
5631
  documentationUrl: urls?.documentationUrl ?? ""
5604
5632
  };
5605
- } else if (result.recommendation === "step_up_required") {
5633
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
5606
5634
  result.requiresStepUp = true;
5607
5635
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
5608
5636
  }
@@ -5612,7 +5640,7 @@ async function verify(config, request2, options) {
5612
5640
  }
5613
5641
  return result;
5614
5642
  }
5615
- var DEFAULT_CONFIG, initCheckPerformed, deprecationWarningShown, verificationCache, DEFAULT_CACHE_TTL_SECONDS;
5643
+ var DEFAULT_CONFIG, initCheckPerformed, deprecationWarningShown, verificationCache, DEFAULT_CACHE_TTL_SECONDS, MAX_RETRY_AFTER_MS;
5616
5644
  var init_verify = __esm({
5617
5645
  "src/verify.ts"() {
5618
5646
  "use strict";
@@ -5620,6 +5648,7 @@ var init_verify = __esm({
5620
5648
  init_trust_levels();
5621
5649
  init_version();
5622
5650
  init_well_known();
5651
+ init_failure_posture();
5623
5652
  DEFAULT_CONFIG = {
5624
5653
  apiBaseUrl: "https://astrasync.ai/api",
5625
5654
  // minTrustScore + minTrustScoreForFull deprecated in v2.3.0 — server decides.
@@ -5633,6 +5662,7 @@ var init_verify = __esm({
5633
5662
  deprecationWarningShown = false;
5634
5663
  verificationCache = /* @__PURE__ */ new Map();
5635
5664
  DEFAULT_CACHE_TTL_SECONDS = 60;
5665
+ MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
5636
5666
  }
5637
5667
  });
5638
5668
 
@@ -5755,12 +5785,223 @@ var init_classify = __esm({
5755
5785
  });
5756
5786
 
5757
5787
  // src/edge-core/events.ts
5788
+ async function withBudget(promise, ms) {
5789
+ let timer;
5790
+ try {
5791
+ return await Promise.race([
5792
+ promise,
5793
+ new Promise((resolve) => {
5794
+ timer = setTimeout(() => resolve(void 0), ms);
5795
+ })
5796
+ ]);
5797
+ } catch {
5798
+ return void 0;
5799
+ } finally {
5800
+ if (timer) clearTimeout(timer);
5801
+ }
5802
+ }
5803
+ var destKeyOf, BeaconBuffer, beaconBuffer;
5758
5804
  var init_events = __esm({
5759
5805
  "src/edge-core/events.ts"() {
5760
5806
  "use strict";
5807
+ init_http();
5761
5808
  init_metadata_capture();
5762
- init_verify();
5809
+ init_version();
5763
5810
  init_request();
5811
+ destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
5812
+ BeaconBuffer = class {
5813
+ constructor(options = {}) {
5814
+ this.queue = [];
5815
+ this.droppedSinceLast = 0;
5816
+ this.droppedTotal = 0;
5817
+ this.deliveredTotal = 0;
5818
+ this.backoffUntil = 0;
5819
+ this.collapse = /* @__PURE__ */ new Map();
5820
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
5821
+ this.rejectedKeys = /* @__PURE__ */ new Set();
5822
+ this.opts = {
5823
+ maxItems: options.maxItems ?? 1e3,
5824
+ maxBatch: options.maxBatch ?? 100,
5825
+ maxAttempts: options.maxAttempts ?? 3,
5826
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
5827
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
5828
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
5829
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
5830
+ now: options.now ?? Date.now
5831
+ };
5832
+ }
5833
+ /**
5834
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
5835
+ * merged into a held aggregate (sent when the window closes) instead of
5836
+ * becoming its own item.
5837
+ */
5838
+ enqueue(dest, item, collapseKey) {
5839
+ const now = this.opts.now();
5840
+ if (collapseKey) {
5841
+ const entry = this.collapse.get(collapseKey);
5842
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
5843
+ for (const target of [entry.first, entry.held]) {
5844
+ if (target && !target.inFlight && this.queue.includes(target)) {
5845
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
5846
+ return;
5847
+ }
5848
+ }
5849
+ const held = this.push(
5850
+ dest,
5851
+ { ...item, repeatCount: 1 },
5852
+ entry.firstSeenAt + this.opts.collapseWindowMs
5853
+ );
5854
+ entry.held = held;
5855
+ return;
5856
+ }
5857
+ this.collapse.delete(collapseKey);
5858
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
5859
+ const oldest = this.collapse.keys().next().value;
5860
+ if (oldest !== void 0) this.collapse.delete(oldest);
5861
+ }
5862
+ const fresh = { firstSeenAt: now };
5863
+ this.collapse.set(collapseKey, fresh);
5864
+ fresh.first = this.push(dest, item, now);
5865
+ return;
5866
+ }
5867
+ this.push(dest, item, now);
5868
+ }
5869
+ /**
5870
+ * Deliver every eligible queued item. Concurrent callers share ONE
5871
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
5872
+ * so nothing in flight is ever sent twice; items queued while it runs are
5873
+ * picked up by the same drain.
5874
+ */
5875
+ flush() {
5876
+ if (!this.inFlight) {
5877
+ this.inFlight = this.drain().finally(() => {
5878
+ this.inFlight = void 0;
5879
+ });
5880
+ }
5881
+ return this.inFlight;
5882
+ }
5883
+ /** Counters for tests and diagnostics. */
5884
+ stats() {
5885
+ return {
5886
+ queueDepth: this.queue.length,
5887
+ droppedSinceLast: this.droppedSinceLast,
5888
+ droppedTotal: this.droppedTotal,
5889
+ deliveredTotal: this.deliveredTotal,
5890
+ flushing: this.inFlight !== void 0
5891
+ };
5892
+ }
5893
+ push(dest, item, eligibleAt) {
5894
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
5895
+ const entry = {
5896
+ dest,
5897
+ destKey: destKeyOf(dest),
5898
+ item,
5899
+ attempts: 0,
5900
+ eligibleAt,
5901
+ inFlight: false
5902
+ };
5903
+ this.queue.push(entry);
5904
+ return entry;
5905
+ }
5906
+ dropOldest() {
5907
+ if (this.queue.shift()) this.countDropped(1);
5908
+ }
5909
+ countDropped(n) {
5910
+ this.droppedSinceLast += n;
5911
+ this.droppedTotal += n;
5912
+ }
5913
+ async drain() {
5914
+ for (; ; ) {
5915
+ const now = this.opts.now();
5916
+ if (now < this.backoffUntil) return;
5917
+ const batch = this.takeBatch(now);
5918
+ if (batch.length === 0) return;
5919
+ const ok = await this.send(batch);
5920
+ if (!ok) return;
5921
+ }
5922
+ }
5923
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
5924
+ takeBatch(now) {
5925
+ const first = this.queue.find((q) => q.eligibleAt <= now);
5926
+ if (!first) return [];
5927
+ const batch = [];
5928
+ const rest = [];
5929
+ for (const q of this.queue) {
5930
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
5931
+ q.inFlight = true;
5932
+ batch.push(q);
5933
+ } else {
5934
+ rest.push(q);
5935
+ }
5936
+ }
5937
+ this.queue = rest;
5938
+ return batch;
5939
+ }
5940
+ /** POST one batch. Returns false when the drain should pause (backoff). */
5941
+ async send(batch) {
5942
+ const dest = batch[0].dest;
5943
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
5944
+ const reportedDropped = this.droppedSinceLast;
5945
+ const meta = {
5946
+ droppedSinceLast: reportedDropped,
5947
+ queueDepth: this.queue.length,
5948
+ adapter: dest.adapter ?? "edge-core",
5949
+ version: SDK_VERSION
5950
+ };
5951
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
5952
+ const response = await withBudget(
5953
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
5954
+ method: "POST",
5955
+ headers: {
5956
+ "Content-Type": "application/json",
5957
+ ...useKey && { Authorization: `Bearer ${useKey}` }
5958
+ },
5959
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
5960
+ ...controller && { signal: controller.signal }
5961
+ }),
5962
+ this.opts.requestTimeoutMs
5963
+ );
5964
+ if (!response) controller?.abort();
5965
+ const status = response?.status;
5966
+ for (const q of batch) q.inFlight = false;
5967
+ if (status !== void 0 && status >= 200 && status < 300) {
5968
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
5969
+ this.deliveredTotal += batch.length;
5970
+ return true;
5971
+ }
5972
+ if (status === 401 && useKey) {
5973
+ this.rejectedKeys.add(useKey);
5974
+ this.requeue(batch, false);
5975
+ return true;
5976
+ }
5977
+ if (status === 429) {
5978
+ const retryAfter = Number(response?.headers.get("retry-after"));
5979
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
5980
+ this.backoffUntil = this.opts.now() + waitMs;
5981
+ this.requeue(batch, false);
5982
+ return false;
5983
+ }
5984
+ if (status !== void 0 && status >= 400 && status < 500) {
5985
+ this.countDropped(batch.length);
5986
+ return true;
5987
+ }
5988
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
5989
+ this.requeue(batch, true);
5990
+ return false;
5991
+ }
5992
+ /** Put a failed batch back at the head of the queue (order preserved). */
5993
+ requeue(batch, countAttempt) {
5994
+ const keep = [];
5995
+ for (const q of batch) {
5996
+ if (countAttempt) q.attempts += 1;
5997
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
5998
+ else keep.push(q);
5999
+ }
6000
+ this.queue = [...keep, ...this.queue];
6001
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
6002
+ }
6003
+ };
6004
+ beaconBuffer = new BeaconBuffer();
5764
6005
  }
5765
6006
  });
5766
6007
 
@@ -5792,6 +6033,7 @@ var init_express = __esm({
5792
6033
  init_well_known();
5793
6034
  init_metadata_capture();
5794
6035
  init_anonymous_beacon();
6036
+ init_failure_posture();
5795
6037
  DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
5796
6038
  }
5797
6039
  });
@@ -5807,6 +6049,7 @@ var init_nextjs = __esm({
5807
6049
  init_pdlss_pre_check();
5808
6050
  init_metadata_capture();
5809
6051
  init_anonymous_beacon();
6052
+ init_failure_posture();
5810
6053
  init_http_pdlss();
5811
6054
  init_approval_gate();
5812
6055
  init_verified_hop();
@@ -6128,6 +6371,7 @@ var init_mcp2 = __esm({
6128
6371
  init_approval_gate();
6129
6372
  init_stepup_poll();
6130
6373
  init_well_known();
6374
+ init_failure_posture();
6131
6375
  init_mcp_server();
6132
6376
  }
6133
6377
  });
@@ -6216,6 +6460,7 @@ var init_src = __esm({
6216
6460
  "use strict";
6217
6461
  init_trust_levels();
6218
6462
  init_verify();
6463
+ init_failure_posture();
6219
6464
  init_stepup_poll();
6220
6465
  init_settlement();
6221
6466
  init_express();
@@ -6238,6 +6483,8 @@ var init_src = __esm({
6238
6483
  init_runtime_signatures();
6239
6484
  init_verified_hop();
6240
6485
  init_http();
6486
+ init_pdlss_parser();
6487
+ init_schema();
6241
6488
  init_version();
6242
6489
  }
6243
6490
  });
@@ -7116,7 +7363,15 @@ var init_settings_installer = __esm({
7116
7363
  "~/.claude/settings.json",
7117
7364
  "~/.claude/settings.local.json",
7118
7365
  "**/.astrasync/**",
7119
- "~/.astrasync/**"
7366
+ "~/.astrasync/**",
7367
+ // 5.11.0 fleet mode: enrolment + signed-policy cache (user scope is inside
7368
+ // ~/.astrasync already; listed explicitly so the intent survives edits) and
7369
+ // the machine-scope enrolment dirs. `//` = absolute from filesystem root;
7370
+ // Windows paths are matched in POSIX form (C:\ → /c/).
7371
+ "~/.astrasync/fleet/**",
7372
+ "//etc/astrasync/**",
7373
+ "//Library/Application Support/AstraSync/**",
7374
+ "//c/ProgramData/AstraSync/**"
7120
7375
  ];
7121
7376
  GUARD_PERMISSION_DENY_RULES = PERMISSION_DENY_PATHS.map(
7122
7377
  (p) => `Edit(${p})`
@@ -7158,6 +7413,545 @@ var init_settings_installer2 = __esm({
7158
7413
  }
7159
7414
  });
7160
7415
 
7416
+ // src/fleet/client.ts
7417
+ function base(url) {
7418
+ return url.replace(/\/+$/, "");
7419
+ }
7420
+ async function call(url, init, timeoutMs) {
7421
+ const controller = new AbortController();
7422
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
7423
+ try {
7424
+ const res = await sdkFetch(url, { ...init, signal: controller.signal });
7425
+ const text = await res.text();
7426
+ let body = text;
7427
+ try {
7428
+ body = text ? JSON.parse(text) : null;
7429
+ } catch {
7430
+ }
7431
+ return { status: res.status, body, headers: res.headers };
7432
+ } finally {
7433
+ clearTimeout(timer);
7434
+ }
7435
+ }
7436
+ function apiError(status, body, what) {
7437
+ const b = body ?? {};
7438
+ const code = b.code ?? (typeof b.error === "string" ? b.error : void 0);
7439
+ return new FleetApiError(`${what} failed (${status}${code ? ` ${code}` : ""})`, status, code);
7440
+ }
7441
+ async function enrolDevice(apiBaseUrl, request2, timeoutMs = 15e3) {
7442
+ const r = await call(
7443
+ `${base(apiBaseUrl)}/fleet/enrol`,
7444
+ {
7445
+ method: "POST",
7446
+ headers: { "Content-Type": "application/json" },
7447
+ body: JSON.stringify(request2)
7448
+ },
7449
+ timeoutMs
7450
+ );
7451
+ if (r.status !== 201 && r.status !== 200) throw apiError(r.status, r.body, "Enrolment");
7452
+ const data = r.body;
7453
+ if (!data?.deviceId || !data.credential || !data.jwks || !data.policyJws) {
7454
+ throw new FleetApiError("Enrolment response is missing required fields", r.status);
7455
+ }
7456
+ return data;
7457
+ }
7458
+ var FleetApiError;
7459
+ var init_client2 = __esm({
7460
+ "src/fleet/client.ts"() {
7461
+ "use strict";
7462
+ init_http();
7463
+ FleetApiError = class extends Error {
7464
+ constructor(message, status, code) {
7465
+ super(message);
7466
+ this.status = status;
7467
+ this.code = code;
7468
+ this.name = "FleetApiError";
7469
+ }
7470
+ };
7471
+ }
7472
+ });
7473
+
7474
+ // src/fleet/enrolment-store.ts
7475
+ function machineScopeDir(platform2 = process.platform, env = process.env) {
7476
+ if (platform2 === "darwin") return "/Library/Application Support/AstraSync";
7477
+ if (platform2 === "win32") return (0, import_path5.join)(env.ProgramData || "C:\\ProgramData", "AstraSync");
7478
+ return "/etc/astrasync";
7479
+ }
7480
+ function fleetPaths(options = {}) {
7481
+ const home = options.home ?? (0, import_os.homedir)();
7482
+ const machineDir = options.machineDir ?? options.env?.ASTRASYNC_FLEET_MACHINE_DIR ?? machineScopeDir(options.platform, options.env);
7483
+ const userDir = (0, import_path5.join)(home, ".astrasync", "fleet");
7484
+ return {
7485
+ machineDevice: (0, import_path5.join)(machineDir, "fleet.json"),
7486
+ machinePolicy: (0, import_path5.join)(machineDir, "policy.jws"),
7487
+ userDir,
7488
+ userDevice: (0, import_path5.join)(userDir, "device.json"),
7489
+ userPolicy: (0, import_path5.join)(userDir, "policy.jws"),
7490
+ userKeyChain: (0, import_path5.join)(userDir, "keychain.json"),
7491
+ state: (0, import_path5.join)(userDir, "state.json")
7492
+ };
7493
+ }
7494
+ function isEnrolment(value) {
7495
+ const v = value;
7496
+ return !!v && typeof v.deviceId === "string" && typeof v.credential === "string" && typeof v.apiBaseUrl === "string" && !!v.org && typeof v.org.ownerId === "string" && !!v.jwks && Array.isArray(v.jwks.keys);
7497
+ }
7498
+ function loadEnrolment(paths) {
7499
+ const candidates = [
7500
+ [paths.machineDevice, "machine"],
7501
+ [paths.userDevice, "user"]
7502
+ ];
7503
+ for (const [path2, scope] of candidates) {
7504
+ if (!(0, import_fs6.existsSync)(path2)) continue;
7505
+ try {
7506
+ const parsed = JSON.parse((0, import_fs6.readFileSync)(path2, "utf-8"));
7507
+ if (isEnrolment(parsed)) return { enrolment: parsed, scope };
7508
+ } catch {
7509
+ }
7510
+ return { corrupt: true, path: path2 };
7511
+ }
7512
+ return null;
7513
+ }
7514
+ function writeFileMode(path2, content, mode) {
7515
+ (0, import_fs6.mkdirSync)((0, import_path5.dirname)(path2), { recursive: true, mode: 448 });
7516
+ (0, import_fs6.writeFileSync)(path2, content, { encoding: "utf-8", mode });
7517
+ try {
7518
+ (0, import_fs6.chmodSync)(path2, mode);
7519
+ } catch {
7520
+ }
7521
+ }
7522
+ function saveEnrolment(paths, enrolment, scope) {
7523
+ const path2 = scope === "machine" ? paths.machineDevice : paths.userDevice;
7524
+ writeFileMode(path2, JSON.stringify(enrolment, null, 2), scope === "machine" ? 420 : 384);
7525
+ return path2;
7526
+ }
7527
+ function readCachedPolicies(paths) {
7528
+ const out = [];
7529
+ for (const p of [paths.userPolicy, paths.machinePolicy]) {
7530
+ try {
7531
+ if ((0, import_fs6.existsSync)(p)) out.push((0, import_fs6.readFileSync)(p, "utf-8").trim());
7532
+ } catch {
7533
+ }
7534
+ }
7535
+ return out.filter(Boolean);
7536
+ }
7537
+ function writeCachedPolicy(paths, jws) {
7538
+ writeFileMode(paths.userPolicy, jws, 384);
7539
+ }
7540
+ function readKeyChain(paths) {
7541
+ try {
7542
+ const parsed = JSON.parse((0, import_fs6.readFileSync)(paths.userKeyChain, "utf-8"));
7543
+ return Array.isArray(parsed) ? parsed.filter((j) => typeof j === "string") : [];
7544
+ } catch {
7545
+ return [];
7546
+ }
7547
+ }
7548
+ function writeKeyChain(paths, chain) {
7549
+ writeFileMode(paths.userKeyChain, JSON.stringify(chain), 384);
7550
+ }
7551
+ function writeMachinePolicy(paths, jws) {
7552
+ writeFileMode(paths.machinePolicy, jws, 420);
7553
+ }
7554
+ function readState(paths) {
7555
+ try {
7556
+ const parsed = JSON.parse((0, import_fs6.readFileSync)(paths.state, "utf-8"));
7557
+ const c = parsed.counts;
7558
+ return {
7559
+ ...parsed,
7560
+ counts: { allow: c?.allow ?? 0, deny: c?.deny ?? 0, ask: c?.ask ?? 0 }
7561
+ };
7562
+ } catch {
7563
+ return { counts: { allow: 0, deny: 0, ask: 0 } };
7564
+ }
7565
+ }
7566
+ function writeState(paths, state) {
7567
+ try {
7568
+ writeFileMode(paths.state, JSON.stringify(state, null, 2), 384);
7569
+ } catch {
7570
+ }
7571
+ }
7572
+ var import_fs6, import_os, import_path5;
7573
+ var init_enrolment_store = __esm({
7574
+ "src/fleet/enrolment-store.ts"() {
7575
+ "use strict";
7576
+ import_fs6 = require("fs");
7577
+ import_os = require("os");
7578
+ import_path5 = require("path");
7579
+ }
7580
+ });
7581
+
7582
+ // src/fleet/types.ts
7583
+ var FLEET_POLICY_TYP, FLEET_POLICY_AUD, FLEET_POLICY_ISS, FLEET_KEYS_TYP, FLEET_KEYS_AUD, DEFAULT_HEARTBEAT_INTERVAL_SEC;
7584
+ var init_types3 = __esm({
7585
+ "src/fleet/types.ts"() {
7586
+ "use strict";
7587
+ FLEET_POLICY_TYP = "fleet-policy+jwt";
7588
+ FLEET_POLICY_AUD = "astrasync:fleet-policy";
7589
+ FLEET_POLICY_ISS = "astrasync.ai";
7590
+ FLEET_KEYS_TYP = "fleet-keys+jwt";
7591
+ FLEET_KEYS_AUD = "astrasync:fleet-keys";
7592
+ DEFAULT_HEARTBEAT_INTERVAL_SEC = 900;
7593
+ }
7594
+ });
7595
+
7596
+ // src/fleet/policy-verify.ts
7597
+ function sha256Hex(text) {
7598
+ return (0, import_crypto.createHash)("sha256").update(text, "utf8").digest("hex");
7599
+ }
7600
+ function parseSettings(raw) {
7601
+ if (!raw || typeof raw !== "object") throw new FleetPolicyError("settings missing");
7602
+ const s = raw;
7603
+ if (s.posture !== "active" && s.posture !== "passive") {
7604
+ throw new FleetPolicyError("settings.posture must be active or passive");
7605
+ }
7606
+ if (s.failOpen !== void 0 && typeof s.failOpen !== "boolean") {
7607
+ throw new FleetPolicyError("settings.failOpen must be a boolean");
7608
+ }
7609
+ if (s.governMcp !== void 0 && typeof s.governMcp !== "boolean") {
7610
+ throw new FleetPolicyError("settings.governMcp must be a boolean");
7611
+ }
7612
+ if (s.skipTools !== void 0 && !(Array.isArray(s.skipTools) && s.skipTools.every((t) => typeof t === "string"))) {
7613
+ throw new FleetPolicyError("settings.skipTools must be a string array");
7614
+ }
7615
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
7616
+ return {
7617
+ posture: s.posture,
7618
+ failOpen: s.failOpen === true,
7619
+ ...typeof s.governMcp === "boolean" && { governMcp: s.governMcp },
7620
+ ...Array.isArray(s.skipTools) && { skipTools: s.skipTools },
7621
+ ...num(s.heartbeatIntervalSec) && { heartbeatIntervalSec: num(s.heartbeatIntervalSec) },
7622
+ ...num(s.staleAfterSec) && { staleAfterSec: num(s.staleAfterSec) }
7623
+ };
7624
+ }
7625
+ async function verifyFleetPolicy(jws, jwks, options) {
7626
+ let payloadBytes;
7627
+ let header;
7628
+ try {
7629
+ const keySet = (0, import_jose2.createLocalJWKSet)(jwks);
7630
+ const result = await (0, import_jose2.compactVerify)(jws, keySet, { algorithms: ["ES256"] });
7631
+ payloadBytes = result.payload;
7632
+ header = result.protectedHeader;
7633
+ } catch (err) {
7634
+ throw new FleetPolicyError(
7635
+ `signature verification failed (${err instanceof Error ? err.message : String(err)})`
7636
+ );
7637
+ }
7638
+ if (header.typ !== FLEET_POLICY_TYP) throw new FleetPolicyError(`unexpected typ ${header.typ}`);
7639
+ let claims;
7640
+ try {
7641
+ claims = JSON.parse(new TextDecoder().decode(payloadBytes));
7642
+ } catch {
7643
+ throw new FleetPolicyError("payload is not JSON");
7644
+ }
7645
+ if (claims.aud !== FLEET_POLICY_AUD) throw new FleetPolicyError(`unexpected aud ${claims.aud}`);
7646
+ if (claims.iss !== FLEET_POLICY_ISS) throw new FleetPolicyError(`unexpected iss ${claims.iss}`);
7647
+ if (!options.expectedSubs.includes(claims.sub)) {
7648
+ throw new FleetPolicyError("policy was issued for a different organisation or device");
7649
+ }
7650
+ if (!Number.isInteger(claims.ver) || claims.ver < 1) {
7651
+ throw new FleetPolicyError("ver must be a positive integer");
7652
+ }
7653
+ if (options.minVer !== void 0 && claims.ver < options.minVer) {
7654
+ throw new FleetPolicyError(
7655
+ `policy version ${claims.ver} is older than the accepted version ${options.minVer} (rollback)`
7656
+ );
7657
+ }
7658
+ if (typeof claims.yaml !== "string" || sha256Hex(claims.yaml) !== claims.sha256) {
7659
+ throw new FleetPolicyError("policy hash mismatch");
7660
+ }
7661
+ const settings = parseSettings(claims.settings);
7662
+ const parsed = parseYaml(claims.yaml);
7663
+ if (!parsed.policy) {
7664
+ const first = parsed.validation.errors[0];
7665
+ throw new FleetPolicyError(
7666
+ `signed policy is invalid${first ? ` (${first.path}: ${first.message})` : ""}`
7667
+ );
7668
+ }
7669
+ return { kid: header.kid, claims: { ...claims, settings }, policy: parsed.policy, settings, jws };
7670
+ }
7671
+ async function resolveFleetJwks(pinned, chain, ownerId) {
7672
+ let trusted = pinned;
7673
+ let lastSeq = 0;
7674
+ for (const link of chain) {
7675
+ try {
7676
+ const { payload, protectedHeader } = await (0, import_jose2.compactVerify)(
7677
+ link,
7678
+ (0, import_jose2.createLocalJWKSet)(trusted),
7679
+ { algorithms: ["ES256"] }
7680
+ );
7681
+ if (protectedHeader.typ !== FLEET_KEYS_TYP) continue;
7682
+ const c = JSON.parse(new TextDecoder().decode(payload));
7683
+ if (c.aud !== FLEET_KEYS_AUD || c.iss !== FLEET_POLICY_ISS || c.sub !== ownerId) continue;
7684
+ if (!Number.isInteger(c.seq) || c.seq <= lastSeq) continue;
7685
+ const keys = Array.isArray(c.keys) ? c.keys.filter((k) => k && k.kty === "EC" && k.crv === "P-256" && !("d" in k)) : [];
7686
+ if (keys.length === 0) continue;
7687
+ trusted = { keys };
7688
+ lastSeq = c.seq;
7689
+ } catch {
7690
+ }
7691
+ }
7692
+ return trusted;
7693
+ }
7694
+ async function pickNewestValid(candidates, jwks, options) {
7695
+ let best = null;
7696
+ let lastError = new FleetPolicyError("no cached fleet policy");
7697
+ for (const jws of candidates) {
7698
+ try {
7699
+ const v = await verifyFleetPolicy(jws, jwks, options);
7700
+ if (!best || v.claims.ver > best.claims.ver) best = v;
7701
+ } catch (err) {
7702
+ lastError = err instanceof Error ? err : new FleetPolicyError(String(err));
7703
+ }
7704
+ }
7705
+ if (!best) throw lastError;
7706
+ return best;
7707
+ }
7708
+ var import_crypto, import_jose2, FleetPolicyError;
7709
+ var init_policy_verify = __esm({
7710
+ "src/fleet/policy-verify.ts"() {
7711
+ "use strict";
7712
+ import_crypto = require("crypto");
7713
+ import_jose2 = require("jose");
7714
+ init_pdlss_parser();
7715
+ init_types3();
7716
+ FleetPolicyError = class extends Error {
7717
+ constructor(message) {
7718
+ super(message);
7719
+ this.name = "FleetPolicyError";
7720
+ }
7721
+ };
7722
+ }
7723
+ });
7724
+
7725
+ // src/fleet/cli.ts
7726
+ function machineFingerprint() {
7727
+ const candidates = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
7728
+ let raw = "";
7729
+ for (const c of candidates) {
7730
+ try {
7731
+ if ((0, import_fs7.existsSync)(c)) {
7732
+ raw = (0, import_fs7.readFileSync)(c, "utf-8").trim();
7733
+ if (raw) break;
7734
+ }
7735
+ } catch {
7736
+ }
7737
+ }
7738
+ if (!raw) raw = `${(0, import_os2.hostname)()}|${(0, import_os2.platform)()}|${(0, import_os2.arch)()}`;
7739
+ return (0, import_crypto2.createHash)("sha256").update(`astrasync-fleet:${raw}`).digest("hex");
7740
+ }
7741
+ async function runEnroll(options) {
7742
+ const print = options.print ?? ((l) => console.log(l));
7743
+ const token = options.token.trim();
7744
+ if (!token)
7745
+ throw new Error("Usage: astrasync guard enroll <token> [--api-base-url <url>] [--system]");
7746
+ const apiBaseUrl = (options.apiBaseUrl ?? DEFAULT_FLEET_API_BASE_URL).replace(/\/+$/, "");
7747
+ const paths = options.paths ?? fleetPaths();
7748
+ const response = await enrolDevice(apiBaseUrl, {
7749
+ token,
7750
+ fingerprint: options.fingerprint ?? machineFingerprint(),
7751
+ hostname: (0, import_os2.hostname)(),
7752
+ os: { platform: (0, import_os2.platform)(), release: (0, import_os2.release)(), arch: (0, import_os2.arch)() },
7753
+ guardVersion: SDK_VERSION,
7754
+ adapters: options.adapters ?? [
7755
+ { kind: "claude-code", managed: false },
7756
+ { kind: "codex", managed: false }
7757
+ ]
7758
+ });
7759
+ const enrolment = {
7760
+ version: 1,
7761
+ deviceId: response.deviceId,
7762
+ credential: response.credential,
7763
+ org: response.org,
7764
+ apiBaseUrl: response.apiBaseUrl || apiBaseUrl,
7765
+ jwks: response.jwks,
7766
+ heartbeatIntervalSec: response.heartbeatIntervalSec || DEFAULT_HEARTBEAT_INTERVAL_SEC,
7767
+ enrolledAt: (/* @__PURE__ */ new Date()).toISOString()
7768
+ };
7769
+ const verified = await verifyFleetPolicy(response.policyJws, enrolment.jwks, {
7770
+ expectedSubs: [enrolment.org.ownerId, enrolment.deviceId]
7771
+ });
7772
+ const scope = options.system ? "machine" : "user";
7773
+ const savedTo = saveEnrolment(paths, enrolment, scope);
7774
+ if (options.system) writeMachinePolicy(paths, response.policyJws);
7775
+ writeCachedPolicy(paths, response.policyJws);
7776
+ writeKeyChain(paths, []);
7777
+ const state = readState(paths);
7778
+ writeState(paths, {
7779
+ ...state,
7780
+ appliedSha256: verified.claims.sha256,
7781
+ maxVer: Math.max(state.maxVer ?? 0, verified.claims.ver),
7782
+ revoked: false
7783
+ });
7784
+ print(
7785
+ `Enrolled in ${enrolment.org.name ?? enrolment.org.ownerId} (device ${enrolment.deviceId}).`
7786
+ );
7787
+ print(` Enrolment: ${savedTo} (${scope} scope)`);
7788
+ print(` Policy: v${verified.claims.ver}, posture ${verified.settings.posture}`);
7789
+ print(
7790
+ " Local policy.yaml / guard.json files are now ignored \u2014 the organisation policy governs."
7791
+ );
7792
+ return enrolment;
7793
+ }
7794
+ async function getFleetStatus(paths = fleetPaths()) {
7795
+ const loaded = loadEnrolment(paths);
7796
+ if (!loaded) return { enrolled: false };
7797
+ if ("corrupt" in loaded) return { enrolled: true, corrupt: loaded.path };
7798
+ const { enrolment, scope } = loaded;
7799
+ const state = readState(paths);
7800
+ let policy;
7801
+ try {
7802
+ const jwks = await resolveFleetJwks(enrolment.jwks, readKeyChain(paths), enrolment.org.ownerId);
7803
+ const v = await pickNewestValid(readCachedPolicies(paths), jwks, {
7804
+ expectedSubs: [enrolment.org.ownerId, enrolment.deviceId],
7805
+ minVer: state.maxVer
7806
+ });
7807
+ policy = {
7808
+ ver: v.claims.ver,
7809
+ sha256: v.claims.sha256,
7810
+ posture: v.settings.posture,
7811
+ failOpen: v.settings.failOpen
7812
+ };
7813
+ } catch (err) {
7814
+ policy = { error: err instanceof Error ? err.message : String(err) };
7815
+ }
7816
+ return {
7817
+ enrolled: true,
7818
+ scope,
7819
+ org: enrolment.org,
7820
+ deviceId: enrolment.deviceId,
7821
+ apiBaseUrl: enrolment.apiBaseUrl,
7822
+ policy,
7823
+ lastHeartbeatAt: state.lastHeartbeatAt,
7824
+ revoked: state.revoked === true,
7825
+ lastError: state.lastError,
7826
+ pendingCounts: state.counts
7827
+ };
7828
+ }
7829
+ function formatFleetStatus(status) {
7830
+ if (!status.enrolled) return ["Fleet: not enrolled (local policy mode)."];
7831
+ if (status.corrupt) {
7832
+ return [`Fleet: enrolment file ${status.corrupt} is unreadable \u2014 the guard fails closed.`];
7833
+ }
7834
+ const lines = [
7835
+ `Fleet: enrolled in ${status.org?.name ?? status.org?.ownerId} (${status.scope} scope)`,
7836
+ ` Device: ${status.deviceId}`,
7837
+ ` Platform: ${status.apiBaseUrl}`
7838
+ ];
7839
+ if (status.policy && "error" in status.policy) {
7840
+ lines.push(` Policy: NO VALID SIGNED POLICY \u2014 ${status.policy.error} (guard denies)`);
7841
+ } else if (status.policy) {
7842
+ lines.push(
7843
+ ` Policy: v${status.policy.ver} (${status.policy.sha256.slice(0, 12)}\u2026), posture ${status.policy.posture}${status.policy.failOpen ? ", fail-open" : ""}`
7844
+ );
7845
+ }
7846
+ lines.push(` Heartbeat: ${status.lastHeartbeatAt ?? "never"}`);
7847
+ if (status.revoked) {
7848
+ lines.push(" Status: REVOKED by the organisation \u2014 enforcing the last good policy");
7849
+ }
7850
+ if (status.lastError) lines.push(` Last sync error: ${status.lastError}`);
7851
+ return lines;
7852
+ }
7853
+ function managedHookCommand(target, os2) {
7854
+ const bin = target === "claude" ? "astrasync-claude-hook" : "astrasync-codex-hook";
7855
+ if (os2 === "win32") return `"C:\\Program Files\\nodejs\\${bin}.cmd"`;
7856
+ return `/usr/local/bin/${bin}`;
7857
+ }
7858
+ function buildClaudeManagedSettings(os2) {
7859
+ return {
7860
+ allowManagedHooksOnly: true,
7861
+ env: { ASTRASYNC_FLEET_REQUIRED: "1" },
7862
+ hooks: {
7863
+ PreToolUse: [
7864
+ {
7865
+ matcher: HOOK_MATCHER,
7866
+ hooks: [{ type: "command", command: managedHookCommand("claude", os2), timeout: 10 }]
7867
+ }
7868
+ ]
7869
+ },
7870
+ permissions: { deny: [...GUARD_PERMISSION_DENY_RULES] }
7871
+ };
7872
+ }
7873
+ function tomlString(value) {
7874
+ return JSON.stringify(value);
7875
+ }
7876
+ function buildCodexRequirementsToml(os2) {
7877
+ const command = os2 === "win32" ? `set ASTRASYNC_FLEET_REQUIRED=1&& ${managedHookCommand("codex", os2)}` : `ASTRASYNC_FLEET_REQUIRED=1 ${managedHookCommand("codex", os2)}`;
7878
+ return [
7879
+ "# AstraSync Local Guard \u2014 managed hook (generated by `astrasync guard managed-settings --codex`)",
7880
+ `# Install at: ${MANAGED_SETTINGS_LOCATIONS.codex[os2]}`,
7881
+ "allow_managed_hooks_only = true",
7882
+ "",
7883
+ "[features]",
7884
+ "hooks = true",
7885
+ "",
7886
+ "[[hooks.PreToolUse]]",
7887
+ `matcher = ${tomlString(CODEX_HOOK_MATCHER)}`,
7888
+ "",
7889
+ "[[hooks.PreToolUse.hooks]]",
7890
+ 'type = "command"',
7891
+ `command = ${tomlString(command)}`,
7892
+ "timeout = 10",
7893
+ 'statusMessage = "AstraSync Local Guard"',
7894
+ ""
7895
+ ].join("\n");
7896
+ }
7897
+ function buildManagedSettingsFiles(targets) {
7898
+ const out = [];
7899
+ for (const target of targets) {
7900
+ for (const os2 of ["darwin", "linux", "win32"]) {
7901
+ const content = target === "claude" ? `${JSON.stringify(buildClaudeManagedSettings(os2), null, 2)}
7902
+ ` : buildCodexRequirementsToml(os2);
7903
+ out.push({
7904
+ target,
7905
+ os: os2,
7906
+ installPath: MANAGED_SETTINGS_LOCATIONS[target][os2],
7907
+ fileName: `${target}-${os2}-${target === "claude" ? "managed-settings.json" : "requirements.toml"}`,
7908
+ content
7909
+ });
7910
+ }
7911
+ }
7912
+ return out;
7913
+ }
7914
+ function writeManagedSettings(files, outDir) {
7915
+ (0, import_fs7.mkdirSync)(outDir, { recursive: true });
7916
+ return files.map((f) => {
7917
+ const p = (0, import_path6.join)(outDir, f.fileName);
7918
+ (0, import_fs7.writeFileSync)(p, f.content, "utf-8");
7919
+ return p;
7920
+ });
7921
+ }
7922
+ var import_crypto2, import_fs7, import_os2, import_path6, DEFAULT_FLEET_API_BASE_URL, MANAGED_SETTINGS_LOCATIONS;
7923
+ var init_cli = __esm({
7924
+ "src/fleet/cli.ts"() {
7925
+ "use strict";
7926
+ import_crypto2 = require("crypto");
7927
+ import_fs7 = require("fs");
7928
+ import_os2 = require("os");
7929
+ import_path6 = require("path");
7930
+ init_settings_installer();
7931
+ init_settings_installer2();
7932
+ init_version();
7933
+ init_client2();
7934
+ init_enrolment_store();
7935
+ init_policy_verify();
7936
+ init_types3();
7937
+ DEFAULT_FLEET_API_BASE_URL = "https://astrasync.ai/api";
7938
+ MANAGED_SETTINGS_LOCATIONS = {
7939
+ // code.claude.com/docs/en/managed-settings
7940
+ claude: {
7941
+ darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
7942
+ linux: "/etc/claude-code/managed-settings.json",
7943
+ win32: "C:\\Program Files\\ClaudeCode\\managed-settings.json"
7944
+ },
7945
+ // learn.chatgpt.com/docs/enterprise/managed-configuration
7946
+ codex: {
7947
+ darwin: "/etc/codex/requirements.toml",
7948
+ linux: "/etc/codex/requirements.toml",
7949
+ win32: "%ProgramData%\\OpenAI\\Codex\\requirements.toml"
7950
+ }
7951
+ };
7952
+ }
7953
+ });
7954
+
7161
7955
  // src/cli/guard.ts
7162
7956
  var guard_exports = {};
7163
7957
  function createPrompt() {
@@ -7296,6 +8090,9 @@ async function cmdStatus() {
7296
8090
  }
7297
8091
  });
7298
8092
  console.log("\nAstraSync Local Guard Status\n");
8093
+ for (const line of formatFleetStatus(await getFleetStatus(fleetPaths()))) {
8094
+ console.log(` ${line}`);
8095
+ }
7299
8096
  if (upgraded) {
7300
8097
  const config = JSON.parse(fs.readFileSync(`${DEFAULT_CONFIG_DIR}/config`, "utf-8"));
7301
8098
  console.log(` Mode: ${config.mode}`);
@@ -7651,6 +8448,66 @@ function printClaudeCodeStatus() {
7651
8448
  }
7652
8449
  }
7653
8450
  }
8451
+ function flagValue(flag) {
8452
+ const i = process.argv.indexOf(flag);
8453
+ return i !== -1 ? process.argv[i + 1] : void 0;
8454
+ }
8455
+ function positionalAfter(command) {
8456
+ const argv = process.argv;
8457
+ const start = argv.indexOf(command) + 1;
8458
+ for (let i = start; i < argv.length; i++) {
8459
+ if (argv[i].startsWith("--")) {
8460
+ if (argv[i] === "--api-base-url" || argv[i] === "--out") i++;
8461
+ continue;
8462
+ }
8463
+ return argv[i];
8464
+ }
8465
+ return void 0;
8466
+ }
8467
+ async function cmdEnroll() {
8468
+ const token = positionalAfter("enroll") ?? "";
8469
+ try {
8470
+ await runEnroll({
8471
+ token,
8472
+ apiBaseUrl: flagValue("--api-base-url"),
8473
+ system: process.argv.includes("--system")
8474
+ });
8475
+ } catch (err) {
8476
+ console.error(`Enrolment failed: ${err instanceof Error ? err.message : String(err)}`);
8477
+ if (process.argv.includes("--system")) {
8478
+ console.error(
8479
+ " (--system writes the machine-scope enrolment and needs administrator rights)"
8480
+ );
8481
+ }
8482
+ process.exit(1);
8483
+ }
8484
+ }
8485
+ async function cmdFleetStatus() {
8486
+ const status = await getFleetStatus(fleetPaths());
8487
+ for (const line of formatFleetStatus(status)) console.log(line);
8488
+ }
8489
+ async function cmdManagedSettings() {
8490
+ const targets = [];
8491
+ if (process.argv.includes("--claude")) targets.push("claude");
8492
+ if (process.argv.includes("--codex")) targets.push("codex");
8493
+ if (targets.length === 0) targets.push("claude", "codex");
8494
+ const files = buildManagedSettingsFiles(targets);
8495
+ const outDir = flagValue("--out");
8496
+ if (outDir) {
8497
+ const written = writeManagedSettings(files, outDir);
8498
+ console.log("Managed-settings templates written:");
8499
+ files.forEach((f, i) => console.log(` ${written[i]} \u2192 deploy to ${f.installPath}`));
8500
+ } else {
8501
+ for (const f of files) {
8502
+ console.log(`===== ${f.target} \xB7 ${f.os} \u2192 ${f.installPath}`);
8503
+ console.log(f.content);
8504
+ }
8505
+ }
8506
+ console.log(
8507
+ "Hook commands assume npm\u2019s default global prefix; adjust if your MDM installs elsewhere."
8508
+ );
8509
+ console.log("Machines must also be enrolled: astrasync guard enroll <token> --system");
8510
+ }
7654
8511
  async function main() {
7655
8512
  const command = process.argv[2] === "guard" ? process.argv[3] : process.argv[2];
7656
8513
  switch (command) {
@@ -7687,6 +8544,16 @@ async function main() {
7687
8544
  case "uninstall-codex":
7688
8545
  await cmdUninstallCodex();
7689
8546
  break;
8547
+ case "enroll":
8548
+ case "enrol":
8549
+ await cmdEnroll();
8550
+ break;
8551
+ case "fleet-status":
8552
+ await cmdFleetStatus();
8553
+ break;
8554
+ case "managed-settings":
8555
+ await cmdManagedSettings();
8556
+ break;
7690
8557
  default:
7691
8558
  console.log("AstraSync Local Guard\n");
7692
8559
  console.log("Usage: astrasync guard <command>\n");
@@ -7707,6 +8574,10 @@ async function main() {
7707
8574
  console.log(" install-codex Install the Local Guard hook for OpenAI Codex CLI");
7708
8575
  console.log(" (same flags; policy shared with Claude Code)");
7709
8576
  console.log(" uninstall-codex Remove the Codex hook (--project for project scope)");
8577
+ console.log(" enroll <token> Enrol this machine in your organisation fleet");
8578
+ console.log(" (--api-base-url <url>, --system for machine scope)");
8579
+ console.log(" fleet-status Show fleet enrolment, signed policy and heartbeat");
8580
+ console.log(" managed-settings Print MDM templates (--claude, --codex, --out <dir>)");
7710
8581
  console.log("");
7711
8582
  process.exit(command ? 1 : 0);
7712
8583
  }
@@ -7727,6 +8598,8 @@ var init_guard = __esm({
7727
8598
  init_git_hooks();
7728
8599
  init_settings_installer();
7729
8600
  init_settings_installer2();
8601
+ init_cli();
8602
+ init_enrolment_store();
7730
8603
  DEFAULT_POLICY_PATH = "./local-policy.yaml";
7731
8604
  DEFAULT_CONFIG_DIR = ".astrasync";
7732
8605
  main().catch((err) => {
@@ -7759,6 +8632,10 @@ COMMANDS:
7759
8632
  verify <id> Look up an agent by ASTRA ID or UUID
7760
8633
  health Check API health
7761
8634
  guard <subcommand> Local Guard CLI (setup, start, status, install-claude-code, ...)
8635
+ guard enroll <token> Enrol this machine in your organisation's fleet
8636
+ (--api-base-url <url>, --system for machine scope)
8637
+ guard fleet-status Fleet enrolment, signed policy and heartbeat state
8638
+ guard managed-settings MDM templates for Claude Code / Codex (--claude, --codex, --out <dir>)
7762
8639
 
7763
8640
  REGISTER OPTIONS:
7764
8641
  --name <name> Agent name (required)