@cotal-ai/auth 0.87.0 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/service.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { registryStores } from "./lifecycle-registry.js";
2
+ import { createToolResultConsentSigner, makeToolResultConsentRecorder } from "./tool-result-consent.js";
1
3
  /**
2
4
  * The AUTH SERVICE daemon — the one server-side process a user-auth space runs alongside its broker
3
5
  * (the delivery-daemon pattern: spawned detached by `cotal up`, pid-filed per space, torn down by
@@ -114,6 +116,8 @@ export const MANAGED_RETIRE_PATH = "/managed-lifecycle/retire";
114
116
  export const VERIFY_ENROLLMENT_PATH = "/manager-service-authority/verify-enrollment";
115
117
  /** Host-only, capability-gated proof decision. It neither signs consent nor serves results. */
116
118
  export const TOOL_RESULT_HUMAN_PROOF_PATH = "/tool-result-human-proof/verify";
119
+ /** Host-only signed consent issuance, never private result content. */
120
+ export const TOOL_RESULT_CONSENT_PATH = "/tool-result-consent/issue";
117
121
  /** Failed-exchange rate limit: at most this many REFUSED exchanges per rolling minute; further
118
122
  * attempts get 429 until the window drains. Successes are unthrottled (the CLI's normal path). */
119
123
  const FAILED_EXCHANGE_PER_MIN = 30;
@@ -424,6 +428,7 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
424
428
  // the FRESH space-manager-lease holder check) and dispatches.
425
429
  const authAdmin = await openAuthAdminListener({ onConnection, server, space, dataAccount, reg: barrierReg, retirement, barrierFlight: retirementFlights, instanceId: authServeInstanceId, epoch: authServeGrant.epoch, grant: authServeGrant, log });
426
430
  unwind.push(() => authAdmin.close());
431
+ const recordToolResultConsent = makeToolResultConsentRecorder(registryStores(registry).authKv, () => refuseIfFenced(), space);
427
432
  const fileArm = ledgerAuthorizeConnect(opts.dir);
428
433
  // The callout's issuance evidence: create-only natkey row, then the connect authority re-read
429
434
  // in the same stream before the minted JWT may leave.
@@ -626,6 +631,7 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
626
631
  refuseIfFenced();
627
632
  return recordIssuance(i);
628
633
  },
634
+ recordToolResultConsent,
629
635
  mintConnectCredential: (args) => {
630
636
  refuseIfFenced();
631
637
  return ensureRootCredential(registry, { ...args, managerInstance: `auth-service:${space}` });
@@ -1731,6 +1737,10 @@ export async function startAuthService(inputs) {
1731
1737
  const port = inputs.port ?? 0;
1732
1738
  if (!Number.isInteger(port) || port < 0 || port > 65535)
1733
1739
  throw new Error(`auth-service: port must be a port number, got ${port}`);
1740
+ if (inputs.toolResultConsents !== undefined &&
1741
+ (!inputs.toolResultConsents || typeof inputs.toolResultConsents.keyId !== "string" ||
1742
+ !/^[A-Za-z0-9_-]{1,200}$/.test(inputs.toolResultConsents.keyId) || typeof inputs.toolResultConsents.resolveAnchor !== "function"))
1743
+ throw new Error("auth-service: tool-result consent signer requires a key id and fresh anchor resolver");
1734
1744
  if (inputs.platformControl !== undefined && typeof inputs.platformControl?.observeAssignment !== "function")
1735
1745
  throw new Error("auth-service: platformControl requires an observeAssignment function");
1736
1746
  const host = inputs.platformControl?.host;
@@ -1756,6 +1766,7 @@ export async function startAuthService(inputs) {
1756
1766
  ...(inputs.platformControl !== undefined ? { platformControl: inputs.platformControl } : {}),
1757
1767
  ...(inputs.platformSupervisor !== undefined ? { platformSupervisor: inputs.platformSupervisor } : {}),
1758
1768
  ...(inputs.standingRenewableTtlSeconds !== undefined ? { standingRenewableTtlSeconds: inputs.standingRenewableTtlSeconds } : {}),
1769
+ ...(inputs.toolResultConsents !== undefined ? { toolResultConsents: inputs.toolResultConsents } : {}),
1759
1770
  });
1760
1771
  return { ...started.handle, readiness: () => ({ ...started.status(), context: inputs.context }) };
1761
1772
  }
@@ -1850,7 +1861,12 @@ async function startAuthContext(o) {
1850
1861
  let publicUrl;
1851
1862
  const failures = []; // rolling-window timestamps of REFUSED exchanges
1852
1863
  const badCaps = []; // rolling-window timestamps of invalid-capability attempts
1864
+ const issueToolResultConsent = o.toolResultConsents === undefined ? undefined : createToolResultConsentSigner({
1865
+ space, accountPublicKey: keys.dataAccount.pub, dir, store: secrets, idp: bridgeIdp, ownerSecret,
1866
+ signer: o.toolResultConsents, record: plane.recordToolResultConsent,
1867
+ });
1853
1868
  const ctx = {
1869
+ ...(issueToolResultConsent ? { issueToolResultConsent } : {}),
1854
1870
  issuer,
1855
1871
  bridge,
1856
1872
  bridgeIdp,
@@ -2289,6 +2305,7 @@ const ROUTES = new Map([
2289
2305
  [MANAGED_RETIRE_PATH, handleManagedLifecycleRetirement],
2290
2306
  [VERIFY_ENROLLMENT_PATH, handleVerifyManagedAgentEnrollment],
2291
2307
  [TOOL_RESULT_HUMAN_PROOF_PATH, handleToolResultHumanProof],
2308
+ [TOOL_RESULT_CONSENT_PATH, handleToolResultConsent],
2292
2309
  ]);
2293
2310
  /** Route one HTTP request against the loopback route table. Errors are JSON `{ error }`. */
2294
2311
  async function handle(req, res, ctx) {
@@ -2734,6 +2751,34 @@ export async function handleToolResultHumanProof(req, res, ctx) {
2734
2751
  return send(res, 403, { error: "fresh signed password proof required" });
2735
2752
  }
2736
2753
  }
2754
+ /** Consent issuance is confined to the authenticated host face and uses sanitized refusals. */
2755
+ export async function handleToolResultConsent(req, res, ctx) {
2756
+ if (req.method !== "POST")
2757
+ return send(res, 405, { error: "POST only" });
2758
+ if (req.headers.origin !== undefined)
2759
+ return send(res, 403, { error: "browser-origin requests are not served here" });
2760
+ if (!/^application\/json\b/.test(req.headers["content-type"] ?? ""))
2761
+ return send(res, 415, { error: "content-type must be application/json" });
2762
+ if (req.headers.authorization !== `Bearer ${ctx.cap}`)
2763
+ return refuseCapability(res, ctx, "missing/invalid exchange capability - consent issuance is a loopback host action");
2764
+ if (!ctx.issueToolResultConsent)
2765
+ return send(res, 503, { error: "tool-result-consent-unavailable", outcome: "not-executed" });
2766
+ let body;
2767
+ try {
2768
+ body = await readJsonBody(req);
2769
+ }
2770
+ catch {
2771
+ return send(res, 400, { error: "invalid consent request", outcome: "not-executed" });
2772
+ }
2773
+ try {
2774
+ return send(res, 200, { consent: await ctx.issueToolResultConsent(body) }, { "cache-control": "no-store" });
2775
+ }
2776
+ catch (error) {
2777
+ const code = error instanceof EpEnvelopeError ? error.code : "unavailable";
2778
+ const status = code === "bad-request" ? 400 : code === "permission-denied" ? 403 : code === "conflict" ? 409 : 503;
2779
+ return send(res, status, { error: `tool-result-consent-${code}`, outcome: error instanceof EpEnvelopeError ? error.outcome ?? "unknown" : "unknown" });
2780
+ }
2781
+ }
2737
2782
  /** The advertised broker address the public bundle carries — the URL participants will dial, so
2738
2783
  * the same scheme family `cotal meshes add` accepts. Exported for the producer/consumer contract
2739
2784
  * smoke; returns the refusal sentence, or undefined when the address is usable. */