@cotal-ai/auth 0.86.0 → 0.87.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/callout.d.ts +4 -2
- package/dist/callout.d.ts.map +1 -1
- package/dist/callout.js +6 -2
- package/dist/callout.js.map +1 -1
- package/dist/idp.d.ts +19 -4
- package/dist/idp.d.ts.map +1 -1
- package/dist/idp.js +19 -1
- package/dist/idp.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/permissions.d.ts +6 -5
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/service.d.ts +27 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +104 -23
- package/dist/service.js.map +1 -1
- package/package.json +4 -4
package/dist/service.js
CHANGED
|
@@ -68,7 +68,7 @@ import { makePlatformControlAuthority, makePlatformControlReadiness, requireMana
|
|
|
68
68
|
import { OwnedConnections } from "./owned-connections.js";
|
|
69
69
|
import { makePlatformSupervisorAuthority, makePlatformSupervisorEnd, platformSupervisorEnded, requirePlatformSupervisorAdmin } from "./platform-supervisor.js";
|
|
70
70
|
import { startAuthCallout } from "./callout.js";
|
|
71
|
-
import { createIdpBridge, verifyIdpToken } from "./idp.js";
|
|
71
|
+
import { createIdpBridge, verifyIdpToken, verifyToolResultHumanProof } from "./idp.js";
|
|
72
72
|
import { PUBLIC_EXCHANGE_VIEWS, assertTransferWriterClaim } from "./token.js";
|
|
73
73
|
import { grantCoordinates, verifySessionRedemption } from "./session-redemption.js";
|
|
74
74
|
import { pinnedJwksResolver } from "./issuer.js";
|
|
@@ -112,6 +112,8 @@ export const MANAGED_RETIRE_PATH = "/managed-lifecycle/retire";
|
|
|
112
112
|
* remote manager (#1972 §2.2). The platform owns the writers; this door owns the decision. It
|
|
113
113
|
* derives the caller's scope from the local ledger and never accepts one from the caller. */
|
|
114
114
|
export const VERIFY_ENROLLMENT_PATH = "/manager-service-authority/verify-enrollment";
|
|
115
|
+
/** Host-only, capability-gated proof decision. It neither signs consent nor serves results. */
|
|
116
|
+
export const TOOL_RESULT_HUMAN_PROOF_PATH = "/tool-result-human-proof/verify";
|
|
115
117
|
/** Failed-exchange rate limit: at most this many REFUSED exchanges per rolling minute; further
|
|
116
118
|
* attempts get 429 until the window drains. Successes are unthrottled (the CLI's normal path). */
|
|
117
119
|
const FAILED_EXCHANGE_PER_MIN = 30;
|
|
@@ -519,18 +521,9 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
519
521
|
return "not registered";
|
|
520
522
|
return "candidate";
|
|
521
523
|
};
|
|
522
|
-
const
|
|
524
|
+
const listManagerInstances = async (owner) => {
|
|
523
525
|
refuseIfFenced();
|
|
524
|
-
|
|
525
|
-
const instanceId = assertLifecycleToken(requested, "managerInstanceId");
|
|
526
|
-
const verdict = await managerGate(owner, instanceId);
|
|
527
|
-
if (verdict !== "candidate")
|
|
528
|
-
throw new Error(`manager instance ${instanceId} is ${verdict}`);
|
|
529
|
-
return instanceId;
|
|
530
|
-
}
|
|
531
|
-
const remoteInstances = [];
|
|
532
|
-
const remoteLive = [];
|
|
533
|
-
let localCandidate;
|
|
526
|
+
const instances = [];
|
|
534
527
|
const localManager = loadLocalManager();
|
|
535
528
|
for (const raw of await scanner.scanManagerGates()) {
|
|
536
529
|
if (raw.op !== undefined)
|
|
@@ -542,16 +535,26 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
542
535
|
const row = parseEndpointGate(raw.data, raw.key);
|
|
543
536
|
if (row.authorityFamily === "environment-manager")
|
|
544
537
|
continue;
|
|
545
|
-
if (row.principal === `${owner}.${remoteManagerActors(instanceId).serve}`)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
else if (localManager?.instanceId === instanceId && row.principal === `local.${localManager.serveIdentity.id}`) {
|
|
551
|
-
if (await managerGate(owner, instanceId) === "candidate")
|
|
552
|
-
localCandidate = instanceId;
|
|
553
|
-
}
|
|
538
|
+
if (row.principal === `${owner}.${remoteManagerActors(instanceId).serve}`)
|
|
539
|
+
instances.push({ instanceId, placement: "remote", state: await managerGate(owner, instanceId) });
|
|
540
|
+
else if (localManager?.instanceId === instanceId && row.principal === `local.${localManager.serveIdentity.id}`)
|
|
541
|
+
instances.push({ instanceId, placement: "local", state: await managerGate(owner, instanceId) });
|
|
554
542
|
}
|
|
543
|
+
return { v: 1, owner, complete: true, instances };
|
|
544
|
+
};
|
|
545
|
+
const selectManagerInstance = async (owner, requested, caller) => {
|
|
546
|
+
refuseIfFenced();
|
|
547
|
+
if (requested !== undefined) {
|
|
548
|
+
const instanceId = assertLifecycleToken(requested, "managerInstanceId");
|
|
549
|
+
const verdict = await managerGate(owner, instanceId);
|
|
550
|
+
if (verdict !== "candidate")
|
|
551
|
+
throw new Error(`manager instance ${instanceId} is ${verdict}`);
|
|
552
|
+
return instanceId;
|
|
553
|
+
}
|
|
554
|
+
const { instances } = await listManagerInstances(owner);
|
|
555
|
+
const remoteInstances = instances.filter((i) => i.placement === "remote").map((i) => i.instanceId);
|
|
556
|
+
const remoteLive = instances.filter((i) => i.placement === "remote" && i.state === "candidate").map((i) => i.instanceId);
|
|
557
|
+
const localCandidate = instances.findLast((i) => i.placement === "local" && i.state === "candidate")?.instanceId;
|
|
555
558
|
remoteLive.sort();
|
|
556
559
|
if (remoteLive.length === 1)
|
|
557
560
|
return remoteLive[0];
|
|
@@ -628,6 +631,7 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
628
631
|
return ensureRootCredential(registry, { ...args, managerInstance: `auth-service:${space}` });
|
|
629
632
|
},
|
|
630
633
|
selectManagerInstance,
|
|
634
|
+
listManagerInstances,
|
|
631
635
|
authorizeManagerCaller: async (owner, instanceId) => {
|
|
632
636
|
refuseIfFenced();
|
|
633
637
|
const verdict = await managerGate(owner, assertLifecycleToken(instanceId, "managerInstanceId"));
|
|
@@ -1878,6 +1882,7 @@ async function startAuthContext(o) {
|
|
|
1878
1882
|
dir,
|
|
1879
1883
|
mintConnectCredential: plane.mintConnectCredential,
|
|
1880
1884
|
selectManagerInstance: plane.selectManagerInstance,
|
|
1885
|
+
listManagerInstances: plane.listManagerInstances,
|
|
1881
1886
|
verifySession: plane.verifySession,
|
|
1882
1887
|
};
|
|
1883
1888
|
const loopback = createServer((req, res) => void handle(req, res, ctx));
|
|
@@ -2279,16 +2284,18 @@ const ROUTES = new Map([
|
|
|
2279
2284
|
],
|
|
2280
2285
|
["/exchange", (req, res, ctx) => handleExchange(req, res, ctx, LOOPBACK_POLICY)],
|
|
2281
2286
|
["/manager-service-authority", (req, res, ctx) => handleManagerServiceAuthority(req, res, ctx, LOOPBACK_POLICY)],
|
|
2287
|
+
["/manager-instances", (req, res, ctx) => handleManagerInstances(req, res, ctx, LOOPBACK_POLICY)],
|
|
2282
2288
|
[INTERACTIVE_RETIRE_PATH, handleInteractiveLifecycleRetirement],
|
|
2283
2289
|
[MANAGED_RETIRE_PATH, handleManagedLifecycleRetirement],
|
|
2284
2290
|
[VERIFY_ENROLLMENT_PATH, handleVerifyManagedAgentEnrollment],
|
|
2291
|
+
[TOOL_RESULT_HUMAN_PROOF_PATH, handleToolResultHumanProof],
|
|
2285
2292
|
]);
|
|
2286
2293
|
/** Route one HTTP request against the loopback route table. Errors are JSON `{ error }`. */
|
|
2287
2294
|
async function handle(req, res, ctx) {
|
|
2288
2295
|
try {
|
|
2289
2296
|
const route = ROUTES.get(req.url ?? "");
|
|
2290
2297
|
if (!route)
|
|
2291
|
-
return send(res, 404, { error: "unknown path - /health, /jwks, /exchange, /manager-service-authority, /manager-service-authority/verify-enrollment, /interactive-lifecycle/retire, /managed-lifecycle/retire" });
|
|
2298
|
+
return send(res, 404, { error: "unknown path - /health, /jwks, /exchange, /manager-service-authority, /manager-instances, /manager-service-authority/verify-enrollment, /interactive-lifecycle/retire, /managed-lifecycle/retire" });
|
|
2292
2299
|
await route(req, res, ctx);
|
|
2293
2300
|
}
|
|
2294
2301
|
catch (e) {
|
|
@@ -2613,6 +2620,45 @@ async function handleExchange(req, res, ctx, policy) {
|
|
|
2613
2620
|
return send(res, 401, { error: reason });
|
|
2614
2621
|
}
|
|
2615
2622
|
}
|
|
2623
|
+
/** The signed-in person's own Manager instances, read-only, on both faces. The proof is the IdP JWT
|
|
2624
|
+
* the human exchange verifies, and the owner is derived from it the same way, so a caller lists only
|
|
2625
|
+
* its own owner's gates. A browser-origin request is refused as on every exchange route; an
|
|
2626
|
+
* embedding's server-side bridge forwards the person's proof. Issuance still re-checks the chosen
|
|
2627
|
+
* instance. */
|
|
2628
|
+
async function handleManagerInstances(req, res, ctx, policy) {
|
|
2629
|
+
if (req.method !== "POST")
|
|
2630
|
+
return send(res, 405, { error: "POST only" });
|
|
2631
|
+
if (req.headers.origin !== undefined)
|
|
2632
|
+
return send(res, 403, { error: "browser-origin requests are not served here" });
|
|
2633
|
+
if (!/^application\/json\b/.test(req.headers["content-type"] ?? ""))
|
|
2634
|
+
return send(res, 415, { error: "content-type must be application/json" });
|
|
2635
|
+
if (policy.requireCapability && req.headers.authorization !== `Bearer ${ctx.cap}`)
|
|
2636
|
+
return refuseCapability(res, ctx, "missing/invalid exchange capability - read it from the space's auth-service.json");
|
|
2637
|
+
const peer = policy.peerKey(req);
|
|
2638
|
+
const peerThrottled = policy.throttled(ctx, peer);
|
|
2639
|
+
const body = await readJsonBody(req);
|
|
2640
|
+
if (typeof body?.idpToken !== "string" || !body.idpToken)
|
|
2641
|
+
return send(res, 400, { error: "the manager instance list needs { idpToken: string }" });
|
|
2642
|
+
let owner;
|
|
2643
|
+
try {
|
|
2644
|
+
owner = deriveOwnerForIdpSubject(ctx.ownerSecret, ctx.bridgeIdp.issuer, (await verifyIdpToken(body.idpToken, ctx.bridgeIdp)).sub);
|
|
2645
|
+
}
|
|
2646
|
+
catch (e) {
|
|
2647
|
+
policy.recordFailure(ctx, peer);
|
|
2648
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2649
|
+
console.error(`auth-service: refused a manager instance list: ${reason}`);
|
|
2650
|
+
if (peerThrottled)
|
|
2651
|
+
return send(res, 429, { error: "too many refused exchanges - wait a minute and retry" });
|
|
2652
|
+
return send(res, 401, { error: reason });
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
return send(res, 200, await ctx.listManagerInstances(owner));
|
|
2656
|
+
}
|
|
2657
|
+
catch (e) {
|
|
2658
|
+
// The proof was good and the gate read was not: unavailable, never an empty list.
|
|
2659
|
+
return send(res, 503, { error: `the manager instance list is unavailable: ${e instanceof Error ? e.message : String(e)}` });
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2616
2662
|
/** The typed manager authority exchange, served on both faces. The face's policy decides whether the
|
|
2617
2663
|
* loopback capability is demanded; on the public face the verified IdP JWT is the proof, and the
|
|
2618
2664
|
* dispatcher reads the caller's scope from the ledger either way. */
|
|
@@ -2654,6 +2700,40 @@ export async function handleManagerServiceAuthority(req, res, ctx, policy) {
|
|
|
2654
2700
|
return send(res, 403, { error: reason });
|
|
2655
2701
|
}
|
|
2656
2702
|
}
|
|
2703
|
+
/** Decide one signed password proof against the host's authenticated session subject.
|
|
2704
|
+
* The subject is an equality constraint, never the source of the derived owner. */
|
|
2705
|
+
export async function handleToolResultHumanProof(req, res, ctx) {
|
|
2706
|
+
if (req.method !== "POST")
|
|
2707
|
+
return send(res, 405, { error: "POST only" });
|
|
2708
|
+
if (req.headers.origin !== undefined)
|
|
2709
|
+
return send(res, 403, { error: "browser-origin requests are not served here" });
|
|
2710
|
+
if (!/^application\/json\b/.test(req.headers["content-type"] ?? ""))
|
|
2711
|
+
return send(res, 415, { error: "content-type must be application/json" });
|
|
2712
|
+
if (req.headers.authorization !== `Bearer ${ctx.cap}`)
|
|
2713
|
+
return refuseCapability(res, ctx, "missing/invalid exchange capability - human proof verification is a loopback host action");
|
|
2714
|
+
let body;
|
|
2715
|
+
try {
|
|
2716
|
+
body = await readJsonBody(req);
|
|
2717
|
+
}
|
|
2718
|
+
catch (error) {
|
|
2719
|
+
return send(res, error instanceof RequestBodyError ? error.status : 400, { error: "invalid human proof request" });
|
|
2720
|
+
}
|
|
2721
|
+
if (!body || typeof body !== "object" || Array.isArray(body) ||
|
|
2722
|
+
Object.keys(body).some(key => key !== "idpToken" && key !== "subject") ||
|
|
2723
|
+
!("idpToken" in body) || typeof body.idpToken !== "string" || !body.idpToken ||
|
|
2724
|
+
!("subject" in body) || typeof body.subject !== "string" || !body.subject)
|
|
2725
|
+
return send(res, 400, { error: "human proof verification needs only { idpToken, subject }" });
|
|
2726
|
+
try {
|
|
2727
|
+
const proof = await verifyToolResultHumanProof(body.idpToken, ctx.bridgeIdp);
|
|
2728
|
+
if (proof.sub !== body.subject)
|
|
2729
|
+
return send(res, 403, { error: "human proof subject does not match the session" });
|
|
2730
|
+
const owner = deriveOwnerForIdpSubject(ctx.ownerSecret, ctx.bridgeIdp.issuer, proof.sub);
|
|
2731
|
+
return send(res, 200, { authorized: true, owner, ...proof });
|
|
2732
|
+
}
|
|
2733
|
+
catch {
|
|
2734
|
+
return send(res, 403, { error: "fresh signed password proof required" });
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2657
2737
|
/** The advertised broker address the public bundle carries — the URL participants will dial, so
|
|
2658
2738
|
* the same scheme family `cotal meshes add` accepts. Exported for the producer/consumer contract
|
|
2659
2739
|
* smoke; returns the refusal sentence, or undefined when the address is usable. */
|
|
@@ -2756,6 +2836,7 @@ function makePublicHandler(ctx, policy, bundle) {
|
|
|
2756
2836
|
],
|
|
2757
2837
|
["/exchange", (req, res, c) => handleExchange(req, res, c, policy)],
|
|
2758
2838
|
["/manager-service-authority", (req, res, c) => handleManagerServiceAuthority(req, res, c, policy)],
|
|
2839
|
+
["/manager-instances", (req, res, c) => handleManagerInstances(req, res, c, policy)],
|
|
2759
2840
|
[
|
|
2760
2841
|
"/.well-known/cotal-mesh",
|
|
2761
2842
|
(req, res) => {
|
|
@@ -2788,7 +2869,7 @@ function makePublicHandler(ctx, policy, bundle) {
|
|
|
2788
2869
|
try {
|
|
2789
2870
|
const route = routes.get(req.url ?? "");
|
|
2790
2871
|
if (!route)
|
|
2791
|
-
return send(res, 404, { error: "unknown path - /health, /jwks, /exchange, /manager-service-authority, /.well-known/cotal-mesh" });
|
|
2872
|
+
return send(res, 404, { error: "unknown path - /health, /jwks, /exchange, /manager-service-authority, /manager-instances, /.well-known/cotal-mesh" });
|
|
2792
2873
|
await route(req, res, ctx);
|
|
2793
2874
|
}
|
|
2794
2875
|
catch (e) {
|