@cotal-ai/auth 0.91.0 → 0.92.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/authority-client.d.ts +70 -3
- package/dist/authority-client.d.ts.map +1 -1
- package/dist/authority-client.js +109 -8
- package/dist/authority-client.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +37 -10
- package/dist/commands.js.map +1 -1
- package/dist/manager-authority.d.ts +59 -3
- package/dist/manager-authority.d.ts.map +1 -1
- package/dist/manager-authority.js +185 -4
- package/dist/manager-authority.js.map +1 -1
- package/dist/native-service-gate.d.ts +14 -3
- package/dist/native-service-gate.d.ts.map +1 -1
- package/dist/native-service-gate.js +34 -20
- package/dist/native-service-gate.js.map +1 -1
- package/dist/native-service-name.d.ts +7 -0
- package/dist/native-service-name.d.ts.map +1 -1
- package/dist/native-service-name.js +15 -6
- package/dist/native-service-name.js.map +1 -1
- package/dist/platform-control.d.ts.map +1 -1
- package/dist/platform-control.js +1 -0
- package/dist/platform-control.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +8 -0
- package/dist/provider.js.map +1 -1
- package/dist/run-cancel-observation.d.ts +50 -0
- package/dist/run-cancel-observation.d.ts.map +1 -0
- package/dist/run-cancel-observation.js +146 -0
- package/dist/run-cancel-observation.js.map +1 -0
- package/dist/service-custody.d.ts +29 -20
- package/dist/service-custody.d.ts.map +1 -1
- package/dist/service-custody.js +37 -167
- package/dist/service-custody.js.map +1 -1
- package/dist/service-endpoint-authority.d.ts +70 -0
- package/dist/service-endpoint-authority.d.ts.map +1 -0
- package/dist/service-endpoint-authority.js +257 -0
- package/dist/service-endpoint-authority.js.map +1 -0
- package/dist/service-host-contract.d.ts +158 -16
- package/dist/service-host-contract.d.ts.map +1 -1
- package/dist/service-host-contract.js +330 -1
- package/dist/service-host-contract.js.map +1 -1
- package/dist/service-host-ops.d.ts +550 -0
- package/dist/service-host-ops.d.ts.map +1 -0
- package/dist/service-host-ops.js +2180 -0
- package/dist/service-host-ops.js.map +1 -0
- package/dist/service-write.d.ts +141 -0
- package/dist/service-write.d.ts.map +1 -0
- package/dist/service-write.js +179 -0
- package/dist/service-write.js.map +1 -0
- package/dist/service.d.ts +54 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +313 -14
- package/dist/service.js.map +1 -1
- package/package.json +9 -4
package/dist/service.js
CHANGED
|
@@ -29,7 +29,8 @@ import { createToolResultConsentSigner, makeToolResultConsentRecorder } from "./
|
|
|
29
29
|
* 127.0.0.1; TLS terminates at a reverse proxy — in-process TLS was rejected: it duplicates cert
|
|
30
30
|
* renewal and forks deploy). It serves ONLY `GET /health`, `GET /jwks`, `POST /exchange`,
|
|
31
31
|
* `POST /manager-service-authority`, and `GET /.well-known/cotal-mesh` (the generated discovery
|
|
32
|
-
* bundle)
|
|
32
|
+
* bundle), plus, only with `serviceEndpoints`, `/service-endpoint-authority` on both faces;
|
|
33
|
+
* everything else 404s. Its
|
|
33
34
|
* `/exchange` demands NO capability — honestly: the 0600 cap is a same-uid file-ACL boundary with
|
|
34
35
|
* no remote meaning, so requiring its bytes from a remote caller would prove nothing. The proof on
|
|
35
36
|
* the public face is the credential itself: the human arm presents an EdDSA IdP JWT verified
|
|
@@ -62,7 +63,7 @@ import { resolve } from "node:path";
|
|
|
62
63
|
import { connect, credsAuthenticator } from "@nats-io/transport-node";
|
|
63
64
|
import { jetstream, jetstreamManager } from "@nats-io/jetstream";
|
|
64
65
|
import { Kvm } from "@nats-io/kv";
|
|
65
|
-
import { CotalEndpoint, deprovisionAgent, dlvDurable, dlvStream, dmDurable, dmStream, isConcreteChannel, provisionAgentDurables, contractDigest, contractRefToHex, contractStoreContext, endpointToken, fetchContractArtifact, verifyClusterManifest, verifyClusterRoot, admissionBucket, admissionMediatorGrants, assertDerivedOwnerToken, assertLifecycleToken, assertValidOwnerToken, authorizeTrustedServeSnapshot, commitSiblingIssuance, credsClaims, EpEnvelopeError, ensureAuthorityStores, epAuthBucket, isReachable, jwtFromCreds, managedRetirementOpId, mintCreds, mintPublicUserJwt, newIdentity, newTakeoverId, observeHostedRunAttempt, openRecordsBucket, parseEndpointGate, parseServiceSpec, parseServiceStatus, rawDigest, parseSecretStoreIdentity, readCheckpointStatus, readRunAdmission, readRunRevocation, readRunRecord, readSvcRecordLeader, reconcileEndpointGate, replayRunJournal, sameSecretStoreIdentity, recordSpecKey, recordStatusKey, RECORD_KINDS, recordsBucket, remoteManagerActors, retirementFrontierStreams, runDriverCaller, serveIssuanceGateKv, standaloneConnectOpts, STANDING_RENEWABLE_TTL_SEC, withIssuerSession, acceptedReadGrant, actorLedgerSource, connectionAcceptedToken, importNativeSubjectPermissions, mintGeneration, parseActorLedgerSource, writeAcceptedRow, invokeCommand, resolveService, contractArtifactCanonicalBytes, DEV_OWNER, endpointRegistrationBarrier, mintLifecycleUid, principalKey, provisionEndpointGateOpen, publishContractArtifact, registerServiceInstance, registerServingInstance, epgateKey, isCasLoss, sameManagedGrantCustody } from "@cotal-ai/core";
|
|
66
|
+
import { CotalEndpoint, deprovisionAgent, dlvDurable, dlvStream, dmDurable, dmStream, isConcreteChannel, provisionAgentDurables, contractDigest, contractRefToHex, contractStoreContext, endpointToken, fetchContractArtifact, verifyClusterManifest, verifyClusterRoot, admissionBucket, admissionMediatorGrants, assertDerivedOwnerToken, assertLifecycleToken, assertValidOwnerToken, authorizeTrustedServeSnapshot, commitSiblingIssuance, credsClaims, EpEnvelopeError, ensureAuthorityStores, epAuthBucket, isReachable, jwtFromCreds, managedRetirementOpId, mintCreds, mintPublicUserJwt, newIdentity, newTakeoverId, observeHostedRunAttempt, openRecordsBucket, parseEndpointGate, parseServiceSpec, parseServiceStatus, rawDigest, parseSecretStoreIdentity, readCheckpointStatus, readRunAdmission, readRunRevocation, readRunRecord, readSvcRecordLeader, reconcileEndpointGate, replayRunJournal, sameSecretStoreIdentity, recordSpecKey, recordStatusKey, RECORD_KINDS, recordsBucket, remoteManagerActors, retirementFrontierStreams, runDriverCaller, serveIssuanceGateKv, standaloneConnectOpts, STANDING_RENEWABLE_TTL_SEC, withIssuerSession, acceptedReadGrant, actorLedgerSource, connectionAcceptedToken, importNativeSubjectPermissions, mintGeneration, parseActorLedgerSource, writeAcceptedRow, invokeCommand, resolveService, contractArtifactCanonicalBytes, DEV_OWNER, endpointRegistrationBarrier, mintLifecycleUid, principalKey, provisionEndpointGateOpen, publishContractArtifact, registerServiceInstance, registerServingInstance, parseServiceAuthorityEnvelope, SERVICE_AUTHORITY_MAX_BYTES, epgateKey, isCasLoss, sameManagedGrantCustody, parseServiceRegistrationWitness, serviceWitnessDigest } from "@cotal-ai/core";
|
|
66
67
|
import { findCotalRoot, loadManagerInstanceIdentity, userAuthStateDir, workspaceSecretStore, createAuthInstanceIdentity, loadAuthInstanceIdentity } from "@cotal-ai/workspace";
|
|
67
68
|
import { decodeJwt } from "jose";
|
|
68
69
|
import { deriveOwnerForIdpSubject, platformControlOwner } from "./derive.js";
|
|
@@ -71,11 +72,11 @@ import { OwnedConnections } from "./owned-connections.js";
|
|
|
71
72
|
import { makePlatformSupervisorAuthority, makePlatformSupervisorEnd, platformSupervisorEnded, requirePlatformSupervisorAdmin } from "./platform-supervisor.js";
|
|
72
73
|
import { startAuthCallout } from "./callout.js";
|
|
73
74
|
import { createIdpBridge, verifyIdpToken, verifyToolResultHumanProof } from "./idp.js";
|
|
74
|
-
import { PUBLIC_EXCHANGE_VIEWS, assertTransferWriterClaim } from "./token.js";
|
|
75
|
+
import { PUBLIC_EXCHANGE_VIEWS, assertTransferWriterClaim, validateUserToken } from "./token.js";
|
|
75
76
|
import { grantCoordinates, verifySessionRedemption } from "./session-redemption.js";
|
|
76
77
|
import { pinnedJwksResolver } from "./issuer.js";
|
|
77
78
|
import { calloutPermissions } from "./permissions.js";
|
|
78
|
-
import { admitRemoteRun, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, holdsRunFence, issueRemoteManagerAuthority, observedRunRequest, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest, parseRemoteRunRevokeRequest, revokeRemoteRun } from "./manager-authority.js";
|
|
79
|
+
import { handleRemoteRunCancel, parseRemoteRunCancelRequest, admitRemoteRun, handleRemoteRunSubmission, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, holdsRunFence, issueRemoteManagerAuthority, observedRunRequest, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest, parseRemoteRunRevokeRequest, revokeRemoteRun } from "./manager-authority.js";
|
|
79
80
|
import { authorizeRemoteRetainedAgentValidation, completeRemoteRetainedAgentValidation, remoteManagerCurrentRegistrationProof } from "./retained-manager-validation.js";
|
|
80
81
|
import { authorizeRemoteManagedAgentEnrollment, authorizeRemoteManagedAgentPrepareRetirement, authorizeRemoteManagedAgentRuntimeCreate, authorizeRemoteManagedAgentRuntimeStatus } from "./managed-agent-enrollment.js";
|
|
81
82
|
import { authorizeRemoteManagerGoalIndexScan, completeRemoteManagerGoalIndexScan } from "./manager-goal-index.js";
|
|
@@ -83,12 +84,16 @@ import { authorizeRemoteManagerAdmin } from "./manager-admin-authorization.js";
|
|
|
83
84
|
import { authorizeRemoteManagerMaintenance, completeRemoteManagerMaintenance } from "./manager-maintenance.js";
|
|
84
85
|
import { validateRetainedManagedAgent } from "./continuity.js";
|
|
85
86
|
import { reconstructRemoteManagerServeGrant, remoteManagerServeGrantFromCluster, remoteManagerSurface } from "./manager-contract.js";
|
|
86
|
-
import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, platformReadinessGrants, remoteManagerIssuerGrants, remoteManagerRegistrationProof, registrationExecutorGrants, servedRunRequestSubjects } from "./authority-client.js";
|
|
87
|
+
import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, platformReadinessGrants, remoteManagerIssuerGrants, remoteManagerRegistrationProof, registrationExecutorGrants, servedRunRequestSubjects, servedRunCancelRequestSubject, serviceCalloutWriterGrants, serviceHostDiscoveryGrants } from "./authority-client.js";
|
|
88
|
+
import { createNativeServiceCalloutAuthority } from "./native-service-callout.js";
|
|
87
89
|
import { authorizeConnectCredential } from "./connect-reader.js";
|
|
88
90
|
import { chatAwareAclResolver, openChatSessionAuthority } from "./chat-session-authority.js";
|
|
89
91
|
import { makeNativeIssuanceRecorder, pruneNativeIssuance } from "./native-issuance.js";
|
|
90
92
|
import { ensureRootCredential } from "./root-credential.js";
|
|
91
93
|
import { activateLifecycleAtUid, observeGate, openLifecycleRegistry, readLifecycleHeadForOperation } from "./lifecycle-registry.js";
|
|
94
|
+
import { openServiceHostEffect, openServiceHostOperations } from "./service-host-ops.js";
|
|
95
|
+
import { SERVICE_ENDPOINT_AUTHORITY_PATH, answerServiceAuthority, serviceAuthorityDenied, serviceAuthorityInfo } from "./service-endpoint-authority.js";
|
|
96
|
+
import { validateServiceRenewalProof } from "./service-renewal.js";
|
|
92
97
|
import { openAuthLedgerScannerCandidate } from "./ledger-scanner.js";
|
|
93
98
|
import { openRecordsScannerCandidate } from "./records-scanner.js";
|
|
94
99
|
import { acquirePlaneClaim, makeDeliveryAdminPlaneOracle, makeDeliveryAdminPrincipalOracle, scannerDeathCopy, planeClaimRefusal } from "./plane-claim.js";
|
|
@@ -98,6 +103,8 @@ import { resumeAgentRetirement, runAgentRetirementBarrier } from "./retirement-b
|
|
|
98
103
|
import { makeRetirementCleaners } from "./retirement-cleaner.js";
|
|
99
104
|
import { makeDrainRepairers } from "./drain-repair.js";
|
|
100
105
|
import { serializedFor } from "./serialized.js";
|
|
106
|
+
import { RunCancelObservations } from "./run-cancel-observation.js";
|
|
107
|
+
import { readCancellationAdmission } from "@cotal-ai/core";
|
|
101
108
|
import { joinOrStartRetirement, openAuthAdminListener } from "./auth-admin.js";
|
|
102
109
|
import { AUTH_SERVICE_ENDPOINT, authClusterArtifacts, authContractArtifactValues } from "./auth-service-contract.js";
|
|
103
110
|
import { drainTargetForEndpoint, openAdmissionMediator } from "./admission-mediator.js";
|
|
@@ -254,6 +261,28 @@ function runtimeSelection(options) {
|
|
|
254
261
|
export async function openAuthAuthorityPlane(opts) {
|
|
255
262
|
return await openAuthAuthorityPlaneWithCustody(opts);
|
|
256
263
|
}
|
|
264
|
+
/** The hosted door over a plane's service endpoint host: refused before entry unless the context is
|
|
265
|
+
* ready, and mapped to the same engine so the internal accessor still finds it. */
|
|
266
|
+
function hostedServiceEndpoints(host, ready) {
|
|
267
|
+
const door = async (request) => { ready(); return await host(request); };
|
|
268
|
+
const engine = SERVICE_HOST_ENGINES.get(host), callout = SERVICE_CALLOUTS.get(host);
|
|
269
|
+
if (engine !== undefined)
|
|
270
|
+
SERVICE_HOST_ENGINES.set(door, engine);
|
|
271
|
+
if (callout !== undefined)
|
|
272
|
+
SERVICE_CALLOUTS.set(door, callout);
|
|
273
|
+
return door;
|
|
274
|
+
}
|
|
275
|
+
/** Package-internal (not exported from the package index): the engine behind a plane's service
|
|
276
|
+
* endpoint host, whose register admission only a later owner-verifying route may call. */
|
|
277
|
+
const SERVICE_HOST_ENGINES = new WeakMap();
|
|
278
|
+
export function serviceHostEngineOf(host) {
|
|
279
|
+
return SERVICE_HOST_ENGINES.get(host);
|
|
280
|
+
}
|
|
281
|
+
/** Package-internal: the native service callout authority composed with a plane's service host. */
|
|
282
|
+
const SERVICE_CALLOUTS = new WeakMap();
|
|
283
|
+
export function serviceHostCalloutOf(host) {
|
|
284
|
+
return SERVICE_CALLOUTS.get(host);
|
|
285
|
+
}
|
|
257
286
|
/** The context's custody callback is private. Failed construction unwinds from one place. */
|
|
258
287
|
async function openAuthAuthorityPlaneWithCustody(opts, onConnection) {
|
|
259
288
|
const unwind = [];
|
|
@@ -270,6 +299,7 @@ async function openAuthAuthorityPlaneWithCustody(opts, onConnection) {
|
|
|
270
299
|
async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
271
300
|
const interactiveManagerSelection = runtimeSelection(opts);
|
|
272
301
|
const { server, space, dataAccount, log } = opts;
|
|
302
|
+
const cancelObservations = new RunCancelObservations(space, opts.runCancelLimits);
|
|
273
303
|
const standingTtl = opts.standingRenewableTtlSeconds ?? STANDING_RENEWABLE_TTL_SEC;
|
|
274
304
|
if (!Number.isSafeInteger(standingTtl) || standingTtl < 5 || standingTtl > STANDING_RENEWABLE_TTL_SEC) {
|
|
275
305
|
throw new Error(`standingRenewableTtlSeconds must be an integer between 5 and ${STANDING_RENEWABLE_TTL_SEC} seconds (got ${standingTtl})`);
|
|
@@ -367,12 +397,17 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
367
397
|
// Mid-life disconnect = the FENCING event (security req: an owned scanner that dies must not
|
|
368
398
|
// leave the plane half-running). First death wins: fence the guard with the ux state-3 copy,
|
|
369
399
|
// close the sibling so a successor's reclaim isn't blocked by a half-dead pair, log loud.
|
|
400
|
+
let serviceHost;
|
|
370
401
|
const fenceOnDeath = (role, sibling) => {
|
|
371
|
-
if (closing)
|
|
372
|
-
return;
|
|
373
402
|
const copy = scannerDeathCopy(space, role);
|
|
403
|
+
// A closing plane does not go fatal, but a service host call it is still draining stops here.
|
|
404
|
+
if (closing) {
|
|
405
|
+
serviceHost?.trip(copy);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
374
408
|
hold?.fence(copy);
|
|
375
409
|
fatalReason = fatalReason ?? copy;
|
|
410
|
+
serviceHost?.trip(copy);
|
|
376
411
|
fireFatal(copy);
|
|
377
412
|
log(`auth-plane: ${copy}`);
|
|
378
413
|
void sibling().catch(() => { });
|
|
@@ -477,11 +512,15 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
477
512
|
return;
|
|
478
513
|
pruneTimer = setTimeout(() => {
|
|
479
514
|
pruneFlight = pruneNativeIssuance(registry, Math.floor(Date.now() / 1000)).then(schedulePrune, (e) => {
|
|
480
|
-
if (closing)
|
|
481
|
-
return;
|
|
482
515
|
const copy = `native-issuance cleanup failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
516
|
+
// As for a scanner death: a closing plane does not go fatal, but a call it is draining stops.
|
|
517
|
+
if (closing) {
|
|
518
|
+
serviceHost?.trip(copy);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
483
521
|
hold?.fence(copy);
|
|
484
522
|
fatalReason = fatalReason ?? copy;
|
|
523
|
+
serviceHost?.trip(copy);
|
|
485
524
|
fireFatal(copy);
|
|
486
525
|
log(`auth-plane: ${copy}`);
|
|
487
526
|
});
|
|
@@ -490,6 +529,84 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
490
529
|
const stopPrune = async () => { clearTimeout(pruneTimer); await pruneFlight; };
|
|
491
530
|
unwind.push(async () => { closing = true; await stopPrune(); });
|
|
492
531
|
schedulePrune();
|
|
532
|
+
// The service endpoint host, default off. Its discovery client only reads, on the ordinary
|
|
533
|
+
// renewing profile: a broker loss makes its reads fail until it reconnects, and a renewal failure
|
|
534
|
+
// closes it for good, after which decide and revoke answer unavailable until the plane is opened
|
|
535
|
+
// again. The engine shares this plane's hold; both fatal sites above trip it.
|
|
536
|
+
let discoveryClient, calloutWriter;
|
|
537
|
+
// Every effect connection the host opened whose close has not succeeded, kept past its call's
|
|
538
|
+
// settlement: plane close and a failed construction close it again and fail while any remains.
|
|
539
|
+
const openEffects = new Set();
|
|
540
|
+
const closeEffects = async () => {
|
|
541
|
+
const failed = [];
|
|
542
|
+
for (const e of [...openEffects]) {
|
|
543
|
+
try {
|
|
544
|
+
await e.close();
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
failed.push(err);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (failed.length > 0)
|
|
551
|
+
throw new AggregateError(failed, `${failed.length} service host effect connection(s) did not close`);
|
|
552
|
+
};
|
|
553
|
+
if (opts.serviceEndpoints !== undefined) {
|
|
554
|
+
const label = `cotal:service-host-discovery:${space}`;
|
|
555
|
+
const d = discoveryClient = await openAuthorityClient({ onConnection, server, space, dataAccount, label, grants: (id) => serviceHostDiscoveryGrants(space, id), log,
|
|
556
|
+
onRenewalFailure: () => {
|
|
557
|
+
log(`${label}: credential renewal failed - service host discovery is closed until the auth plane is opened again`);
|
|
558
|
+
void discoveryClient?.close().catch(() => { });
|
|
559
|
+
} });
|
|
560
|
+
unwind.push(() => d.close());
|
|
561
|
+
unwind.push(() => closeEffects());
|
|
562
|
+
const discovery = await openLifecycleRegistry(d.nc, space);
|
|
563
|
+
await opts.probeServiceHost?.discovery?.({ registry: discovery, kill: () => d.close() });
|
|
564
|
+
const probeEffect = opts.probeServiceHost?.effect;
|
|
565
|
+
// Serving reads registered closures from the contract store on the issuer's connection, which
|
|
566
|
+
// holds only the store's exact-subject Direct Get.
|
|
567
|
+
const contractStore = await contractStoreContext(remoteIssuer.nc, space);
|
|
568
|
+
const readContractArtifact = async (digest) => {
|
|
569
|
+
const bytes = await fetchContractArtifact(contractStore, contractRefToHex(digest));
|
|
570
|
+
if (!bytes)
|
|
571
|
+
throw new EpEnvelopeError("failed-precondition", `the registered contract artifact ${digest} is not readable`);
|
|
572
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
573
|
+
};
|
|
574
|
+
const engine = await openServiceHostOperations({
|
|
575
|
+
discovery, hold: hold, accountPublicKey: dataAccount.pub, ledgerDir: opts.dir, startup: opts.serviceEndpoints.startup,
|
|
576
|
+
serving: { readContractArtifact },
|
|
577
|
+
openEffect: async (scope) => {
|
|
578
|
+
const opened = await openServiceHostEffect({ onConnection, server, space, dataAccount, log }, scope);
|
|
579
|
+
const effect = { ...opened, close: async () => { await opened.close(); openEffects.delete(effect); } };
|
|
580
|
+
openEffects.add(effect);
|
|
581
|
+
if (probeEffect !== undefined) {
|
|
582
|
+
try {
|
|
583
|
+
await probeEffect(effect, scope);
|
|
584
|
+
}
|
|
585
|
+
catch (e) {
|
|
586
|
+
await effect.close().catch(() => { });
|
|
587
|
+
throw e;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return effect;
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
unwind.push(() => engine.close());
|
|
594
|
+
serviceHost = engine;
|
|
595
|
+
SERVICE_HOST_ENGINES.set(engine.handle, engine);
|
|
596
|
+
// A fence while the engine was opening latches it now.
|
|
597
|
+
if (fatalReason !== undefined)
|
|
598
|
+
engine.trip(fatalReason);
|
|
599
|
+
// The native service callout: a service bearer's connect mints only for the current bound
|
|
600
|
+
// assignment its witness names, read from the engine, on a writer that stages and touches the
|
|
601
|
+
// endpoint gate and credential rows. A fenced plane or a latched engine refuses it.
|
|
602
|
+
const calloutLabel = `cotal:service-callout:${space}`;
|
|
603
|
+
const cw = calloutWriter = await openAuthorityClient({ onConnection, server, space, dataAccount, label: calloutLabel, grants: (id) => serviceCalloutWriterGrants(space, id), log });
|
|
604
|
+
unwind.push(() => cw.close());
|
|
605
|
+
SERVICE_CALLOUTS.set(engine.handle, createNativeServiceCalloutAuthority({
|
|
606
|
+
registry: await openLifecycleRegistry(cw.nc, space), reader: () => reader.current(), dataAccount,
|
|
607
|
+
resolveAssignment: (witness) => engine.serviceBinding(witness), readClusterArtifact: readContractArtifact, assertOpen: () => refuseIfFenced(),
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
493
610
|
const recordsJsm = await jetstreamManager(remoteIssuer.nc);
|
|
494
611
|
const authKv = await new Kvm(remoteIssuer.nc).open(epAuthBucket(space));
|
|
495
612
|
const managerIssuanceGate = (instanceId) => serveIssuanceGateKv(authKv, space, { endpoint: "manager", instanceId });
|
|
@@ -633,6 +750,12 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
633
750
|
// envelope asked (SPEC 14.8): the forward's coordinates are the manager's word. The manager
|
|
634
751
|
// forwards from inside the handler serving the request, so an observation older than the window
|
|
635
752
|
// has no forward left to bind; the window also bounds the table.
|
|
753
|
+
remoteIssuer.nc.subscribe(servedRunCancelRequestSubject(space), {
|
|
754
|
+
callback: (error, message) => {
|
|
755
|
+
if (!error)
|
|
756
|
+
cancelObservations.observe(message.subject, message.data);
|
|
757
|
+
},
|
|
758
|
+
});
|
|
636
759
|
const observedRunRequests = new Map();
|
|
637
760
|
for (const subject of servedRunRequestSubjects(space))
|
|
638
761
|
remoteIssuer.nc.subscribe(subject, {
|
|
@@ -1457,6 +1580,34 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
1457
1580
|
observeManagerGate: observeLegacyManagerGate,
|
|
1458
1581
|
});
|
|
1459
1582
|
},
|
|
1583
|
+
submitManagerRun: async ({ request, ...holder }) => {
|
|
1584
|
+
refuseIfFenced();
|
|
1585
|
+
const { owner } = holder;
|
|
1586
|
+
requireManagerAuthorityHolder(holder, String(request?.instanceId), 'keyed run submission needs scope "supervise"');
|
|
1587
|
+
return withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, session => handleRemoteRunSubmission({
|
|
1588
|
+
request, owner, space, accountPublicKey: dataAccount.pub, proofSecret: dataAccount.signingSeed, endpoint: "manager",
|
|
1589
|
+
observeManagerGate: observeLegacyManagerGate, issued: session.store, sourceIsLive: composedSourceIsLive(session),
|
|
1590
|
+
takeObserved: takeObservedRunRequest,
|
|
1591
|
+
registeredCommand: async (instanceId, registrationRevision, command) => {
|
|
1592
|
+
const registered = remoteManagerSurface(await registeredManagerCluster(owner, instanceId, { registrationRevision }))[command];
|
|
1593
|
+
if (!registered)
|
|
1594
|
+
throw new EpEnvelopeError("failed-precondition", "registered manager has no keyed contract");
|
|
1595
|
+
return registered;
|
|
1596
|
+
},
|
|
1597
|
+
withStore: async (runId, submission, work) => {
|
|
1598
|
+
const nc = await connect({ servers: server, ...standaloneConnectOpts({
|
|
1599
|
+
creds: await mintCreds(issuerAuth(), newIdentity(), "run-admitter", { runAdmitter: { endpoint: "manager", runId, submission }, expiresInSeconds: 60 }), tls: false,
|
|
1600
|
+
}), maxReconnectAttempts: 0 });
|
|
1601
|
+
onConnection?.(nc, `cotal:keyed-admitter:${space}`);
|
|
1602
|
+
try {
|
|
1603
|
+
return await work(await new Kvm(nc).open(admissionBucket(space)), await jetstreamManager(nc));
|
|
1604
|
+
}
|
|
1605
|
+
finally {
|
|
1606
|
+
await nc.drain().catch(() => nc.close());
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
}));
|
|
1610
|
+
},
|
|
1460
1611
|
admitManagerRun: async ({ request, ...holder }) => {
|
|
1461
1612
|
refuseIfFenced();
|
|
1462
1613
|
const { owner } = holder;
|
|
@@ -1489,6 +1640,46 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
1489
1640
|
}
|
|
1490
1641
|
});
|
|
1491
1642
|
},
|
|
1643
|
+
cancelManagerRun: async ({ request, ...holder }) => {
|
|
1644
|
+
refuseIfFenced();
|
|
1645
|
+
const req = parseRemoteRunCancelRequest(request);
|
|
1646
|
+
requireManagerAuthorityHolder(holder, req.instanceId, 'manager run cancellation needs scope "supervise"');
|
|
1647
|
+
return withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, session => handleRemoteRunCancel({
|
|
1648
|
+
request: req, owner: holder.owner, space, endpoint: "manager", accountPublicKey: dataAccount.pub,
|
|
1649
|
+
proofSecret: dataAccount.signingSeed, observeManagerGate: observeLegacyManagerGate,
|
|
1650
|
+
observations: cancelObservations, flush: () => remoteIssuer.nc.flush(), assertServing: refuseIfFenced,
|
|
1651
|
+
issued: session.store, sourceIsLive: composedSourceIsLive(session),
|
|
1652
|
+
interactiveScope: async (caller, sources) => {
|
|
1653
|
+
if (sources.length !== 1 || sources[0].space !== space)
|
|
1654
|
+
return undefined;
|
|
1655
|
+
const source = parseActorLedgerSource(sources[0]);
|
|
1656
|
+
if (!source || source.owner !== caller.owner || source.actor !== caller.actor || source.lifecycleUid !== caller.uid)
|
|
1657
|
+
return undefined;
|
|
1658
|
+
const row = findActorUnified(opts.dir, caller.owner, caller.actor);
|
|
1659
|
+
return row?.kind === "interactive" && row.lifecycleUid === caller.uid ? [...row.scope] : undefined;
|
|
1660
|
+
},
|
|
1661
|
+
registeredCommand: async (instanceId, registrationRevision, command) => {
|
|
1662
|
+
const registered = remoteManagerSurface(await registeredManagerCluster(holder.owner, instanceId, { registrationRevision }))[command];
|
|
1663
|
+
if (!registered)
|
|
1664
|
+
throw new EpEnvelopeError("failed-precondition", "registered manager has no run-cancel contract");
|
|
1665
|
+
return registered;
|
|
1666
|
+
},
|
|
1667
|
+
readAdmission: runId => readCancellationAdmission(recordsJsm, space, "manager", runId),
|
|
1668
|
+
readRevocation: runId => readRunRevocation(recordsJsm, space, "manager", runId),
|
|
1669
|
+
withStore: async (runId, work) => {
|
|
1670
|
+
const nc = await connect({ servers: server, ...standaloneConnectOpts({
|
|
1671
|
+
creds: await mintCreds(issuerAuth(), newIdentity(), "run-admitter", { runAdmitter: { endpoint: "manager", runId }, expiresInSeconds: 60 }), tls: false,
|
|
1672
|
+
}), maxReconnectAttempts: 0 });
|
|
1673
|
+
onConnection?.(nc, `cotal:run-cancel:${space}`);
|
|
1674
|
+
try {
|
|
1675
|
+
return await work(await new Kvm(nc).open(admissionBucket(space)));
|
|
1676
|
+
}
|
|
1677
|
+
finally {
|
|
1678
|
+
await nc.drain().catch(() => nc.close());
|
|
1679
|
+
}
|
|
1680
|
+
},
|
|
1681
|
+
}));
|
|
1682
|
+
},
|
|
1492
1683
|
revokeManagerRun: async ({ request, ...holder }) => {
|
|
1493
1684
|
refuseIfFenced();
|
|
1494
1685
|
const req = parseRemoteRunRevokeRequest(request);
|
|
@@ -1658,14 +1849,24 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
|
|
|
1658
1849
|
return await serveIssuanceGateKv(authKv, space, { endpoint, instanceId }).observe();
|
|
1659
1850
|
},
|
|
1660
1851
|
fenced,
|
|
1852
|
+
...(serviceHost !== undefined ? { serviceEndpointHost: serviceHost.handle } : {}),
|
|
1661
1853
|
close: async () => {
|
|
1662
1854
|
// Clean-close order (SPEC 13.13): the rail stops answering first, then scan-capable
|
|
1663
1855
|
// clients down, then `held → released` (never released while either scanner can still
|
|
1664
1856
|
// act), then the barrier that wrote it. An unreachable release keeps the barrier alive
|
|
1665
1857
|
// for a later close retry; successful scanner teardown is not run a second time.
|
|
1666
1858
|
closing = true;
|
|
1859
|
+
// Before the first await: no service host call enters once close has begun.
|
|
1860
|
+
serviceHost?.refuse();
|
|
1667
1861
|
await stopPrune();
|
|
1668
1862
|
await stopChatSweep();
|
|
1863
|
+
// Every service host call that entered returns, discovery and effects included, then every
|
|
1864
|
+
// effect connection that did not close is closed again, before discovery closes and before
|
|
1865
|
+
// the claim is released. A failure here rejects with the claim still held; close retries it.
|
|
1866
|
+
await serviceHost?.close();
|
|
1867
|
+
await closeEffects();
|
|
1868
|
+
await calloutWriter?.close();
|
|
1869
|
+
await discoveryClient?.close();
|
|
1669
1870
|
const scannerFlight = closeScanners ??= (async () => {
|
|
1670
1871
|
await authAdmin?.close();
|
|
1671
1872
|
const results = await Promise.allSettled([reader.close(), recordsScanner.close(), scanner.close()]);
|
|
@@ -1851,6 +2052,7 @@ export async function runAuthService(args, store, options = {}) {
|
|
|
1851
2052
|
// selection exactly as before. A hosted context passes no local manager at all.
|
|
1852
2053
|
localManager: () => loadManagerInstanceIdentity(root, space),
|
|
1853
2054
|
...(interactiveManagerSelection !== undefined ? { interactiveManagerSelection } : {}),
|
|
2055
|
+
...(options.runCancelLimits !== undefined ? { runCancelLimits: options.runCancelLimits } : {}),
|
|
1854
2056
|
});
|
|
1855
2057
|
// All planes bound — NOW write the discovery file (its existence is the readiness signal).
|
|
1856
2058
|
const { url, publicUrl, cap } = started.handle;
|
|
@@ -1955,6 +2157,7 @@ export async function startAuthService(inputs) {
|
|
|
1955
2157
|
...(inputs.platformSupervisor !== undefined ? { platformSupervisor: inputs.platformSupervisor } : {}),
|
|
1956
2158
|
...(inputs.standingRenewableTtlSeconds !== undefined ? { standingRenewableTtlSeconds: inputs.standingRenewableTtlSeconds } : {}),
|
|
1957
2159
|
...(inputs.toolResultConsents !== undefined ? { toolResultConsents: inputs.toolResultConsents } : {}),
|
|
2160
|
+
...(inputs.serviceEndpoints !== undefined ? { serviceEndpoints: inputs.serviceEndpoints } : {}),
|
|
1958
2161
|
});
|
|
1959
2162
|
return { ...started.handle, readiness: () => ({ ...started.status(), context: inputs.context }) };
|
|
1960
2163
|
}
|
|
@@ -1995,7 +2198,9 @@ async function startAuthContext(o) {
|
|
|
1995
2198
|
log: (l) => console.error(l),
|
|
1996
2199
|
localManager: o.localManager,
|
|
1997
2200
|
...(o.interactiveManagerSelection !== undefined ? { interactiveManagerSelection: o.interactiveManagerSelection } : {}),
|
|
2201
|
+
...(o.runCancelLimits !== undefined ? { runCancelLimits: o.runCancelLimits } : {}),
|
|
1998
2202
|
...(o.standingRenewableTtlSeconds !== undefined ? { standingRenewableTtlSeconds: o.standingRenewableTtlSeconds } : {}),
|
|
2203
|
+
...(o.serviceEndpoints !== undefined ? { serviceEndpoints: o.serviceEndpoints } : {}),
|
|
1999
2204
|
}, onConnection);
|
|
2000
2205
|
let nc;
|
|
2001
2206
|
let http;
|
|
@@ -2020,7 +2225,9 @@ async function startAuthContext(o) {
|
|
|
2020
2225
|
name: `cotal:auth-service:${space}`,
|
|
2021
2226
|
});
|
|
2022
2227
|
onConnection(nc, `cotal:auth-service:${space}`);
|
|
2228
|
+
const serviceCallout = plane.serviceEndpointHost !== undefined ? serviceHostCalloutOf(plane.serviceEndpointHost) : undefined;
|
|
2023
2229
|
startAuthCallout(nc, {
|
|
2230
|
+
...(serviceCallout !== undefined ? { serviceAuthority: serviceCallout } : {}),
|
|
2024
2231
|
xkeySeed: callout.xkey.seed,
|
|
2025
2232
|
authAccount: { pub: callout.account.pub, signingSeed: callout.account.signingSeed },
|
|
2026
2233
|
dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
|
|
@@ -2057,8 +2264,10 @@ async function startAuthContext(o) {
|
|
|
2057
2264
|
space, accountPublicKey: keys.dataAccount.pub, dir, store: secrets, idp: bridgeIdp, ownerSecret,
|
|
2058
2265
|
signer: o.toolResultConsents, record: plane.recordToolResultConsent,
|
|
2059
2266
|
});
|
|
2267
|
+
const serviceEngine = plane.serviceEndpointHost !== undefined ? serviceHostEngineOf(plane.serviceEndpointHost) : undefined;
|
|
2060
2268
|
const ctx = {
|
|
2061
2269
|
...(issueToolResultConsent ? { issueToolResultConsent } : {}),
|
|
2270
|
+
...(serviceEngine !== undefined ? { serviceAuthority: { space, accountPublicKey: keys.dataAccount.pub, engine: serviceEngine, material: issuer } } : {}),
|
|
2062
2271
|
issuer,
|
|
2063
2272
|
bridge,
|
|
2064
2273
|
bridgeIdp,
|
|
@@ -2078,7 +2287,9 @@ async function startAuthContext(o) {
|
|
|
2078
2287
|
scanManagerGoalIndex: plane.scanManagerGoalIndex,
|
|
2079
2288
|
authorizeManagerAdmin: plane.authorizeManagerAdmin,
|
|
2080
2289
|
admitManagerRun: plane.admitManagerRun,
|
|
2290
|
+
submitManagerRun: plane.submitManagerRun,
|
|
2081
2291
|
revokeManagerRun: plane.revokeManagerRun,
|
|
2292
|
+
cancelManagerRun: plane.cancelManagerRun,
|
|
2082
2293
|
issueManagerRunAttempt: plane.issueManagerRunAttempt,
|
|
2083
2294
|
secrets,
|
|
2084
2295
|
retireInteractiveLifecycle: plane.retireInteractiveLifecycle,
|
|
@@ -2263,6 +2474,7 @@ async function startAuthContext(o) {
|
|
|
2263
2474
|
cap,
|
|
2264
2475
|
closed: owned.closed,
|
|
2265
2476
|
connections: () => owned.connections(),
|
|
2477
|
+
...(plane.serviceEndpointHost !== undefined ? { serviceEndpointHost: hostedServiceEndpoints(plane.serviceEndpointHost, refuseUnlessReady) } : {}),
|
|
2266
2478
|
...(o.platformSupervisor !== undefined ? {
|
|
2267
2479
|
platformSupervisorAuthority: async (owner) => {
|
|
2268
2480
|
refuseUnlessReady();
|
|
@@ -2293,6 +2505,7 @@ async function startAuthContext(o) {
|
|
|
2293
2505
|
case "manager-goal-index-scan": return plane.scanManagerGoalIndex({ ...holder, request });
|
|
2294
2506
|
case "manager-admin-authorization": return plane.authorizeManagerAdmin({ ...holder, request });
|
|
2295
2507
|
case "manager-run-admission": return plane.admitManagerRun({ ...holder, request });
|
|
2508
|
+
case "manager-run-submission": return plane.submitManagerRun({ ...holder, request });
|
|
2296
2509
|
case "manager-run-attempt": return plane.issueManagerRunAttempt({ ...holder, request });
|
|
2297
2510
|
case "manager-retained-agent-validation": return validateRetainedForHolder(holder, request);
|
|
2298
2511
|
}
|
|
@@ -2389,6 +2602,16 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
|
|
|
2389
2602
|
return ctx.authorizeManagerAdmin({ owner, scope: row.scope ?? [], request: body.request });
|
|
2390
2603
|
if (request.kind === "manager-run-admission")
|
|
2391
2604
|
return ctx.admitManagerRun({ owner, scope: row.scope ?? [], request: body.request });
|
|
2605
|
+
if (request.kind === "manager-run-submission") {
|
|
2606
|
+
if (!ctx.submitManagerRun)
|
|
2607
|
+
throw new EpEnvelopeError("unimplemented", "issuing host has no keyed submission handler");
|
|
2608
|
+
return ctx.submitManagerRun({ owner, scope: row.scope ?? [], request: body.request });
|
|
2609
|
+
}
|
|
2610
|
+
if (request.kind === "manager-run-cancel") {
|
|
2611
|
+
if (!ctx.cancelManagerRun)
|
|
2612
|
+
throw new EpEnvelopeError("unimplemented", "this issuing host has no run-cancel handler");
|
|
2613
|
+
return ctx.cancelManagerRun({ owner, scope: row.scope ?? [], request: body.request });
|
|
2614
|
+
}
|
|
2392
2615
|
if (request.kind === "manager-run-revoke") {
|
|
2393
2616
|
if (ctx.revokeManagerRun === undefined)
|
|
2394
2617
|
throw new EpEnvelopeError("unimplemented", "this issuing host has no run revoke handler");
|
|
@@ -2503,6 +2726,7 @@ const ROUTES = new Map([
|
|
|
2503
2726
|
["/exchange", (req, res, ctx) => handleExchange(req, res, ctx, LOOPBACK_POLICY)],
|
|
2504
2727
|
["/manager-service-authority", (req, res, ctx) => handleManagerServiceAuthority(req, res, ctx, LOOPBACK_POLICY)],
|
|
2505
2728
|
["/manager-instances", (req, res, ctx) => handleManagerInstances(req, res, ctx, LOOPBACK_POLICY)],
|
|
2729
|
+
[SERVICE_ENDPOINT_AUTHORITY_PATH, (req, res, ctx) => handleServiceEndpointAuthority(req, res, ctx, LOOPBACK_POLICY)],
|
|
2506
2730
|
[INTERACTIVE_RETIRE_PATH, handleInteractiveLifecycleRetirement],
|
|
2507
2731
|
[MANAGED_RETIRE_PATH, handleManagedLifecycleRetirement],
|
|
2508
2732
|
[VERIFY_ENROLLMENT_PATH, handleVerifyManagedAgentEnrollment],
|
|
@@ -2988,6 +3212,80 @@ async function handleManagerInstances(req, res, ctx, policy) {
|
|
|
2988
3212
|
return send(res, 503, { error: `the manager instance list is unavailable: ${e instanceof Error ? e.message : String(e)}` });
|
|
2989
3213
|
}
|
|
2990
3214
|
}
|
|
3215
|
+
/** A service renewal or access proof, verified against this context's own signing keys: the
|
|
3216
|
+
* signed witness it binds. A request that carries a witness must name the proof's; a status query
|
|
3217
|
+
* carries none, so its proof's own witness is the expected one. Current native state is the
|
|
3218
|
+
* engine's check, never the proof's. */
|
|
3219
|
+
async function verifyServiceProof(proof, request, issuer, space) {
|
|
3220
|
+
const key = issuer.localKeySet();
|
|
3221
|
+
if (proof.kind === "service-renewal") {
|
|
3222
|
+
const expected = "witness" in request ? request.witness : parseServiceRegistrationWitness(decodeJwt(proof.token).witness);
|
|
3223
|
+
return (await validateServiceRenewalProof(proof.token, { issuer: issuer.issuer, key, witness: expected })).witness;
|
|
3224
|
+
}
|
|
3225
|
+
const token = await validateUserToken(proof.token, { issuer: issuer.issuer, audience: space, key });
|
|
3226
|
+
if (token.act.view !== "service-endpoint" || token.act.service?.duty !== "serve")
|
|
3227
|
+
throw new Error("the access proof is not a service bearer");
|
|
3228
|
+
const witness = parseServiceRegistrationWitness(token.act.service.witness);
|
|
3229
|
+
if (token.owner !== witness.principal.owner || token.act.owner !== witness.principal.owner || token.act.actor !== witness.principal.actor
|
|
3230
|
+
|| token.act.lifecycleUid !== witness.principal.lifecycleUid || token.credentialId !== witness.rootCredentialId)
|
|
3231
|
+
throw new Error("the access proof does not bind its witness");
|
|
3232
|
+
if ("witness" in request && serviceWitnessDigest(request.witness) !== serviceWitnessDigest(witness))
|
|
3233
|
+
throw new Error("the access proof binds another witness");
|
|
3234
|
+
return witness;
|
|
3235
|
+
}
|
|
3236
|
+
/** The public service endpoint authority route, on both faces and only with `serviceEndpoints`: GET
|
|
3237
|
+
* reads the account info, POST takes one `{request, proof}` envelope. The proof is the credential on
|
|
3238
|
+
* either face; the loopback capability is never read or accepted here. An IdP proof verifies against
|
|
3239
|
+
* the pinned issuer and exact audience, and its subject derives the owner. A service renewal or
|
|
3240
|
+
* access proof verifies against this context's own issuer and binds a witness, never an owner's
|
|
3241
|
+
* authority. A refused proof counts against the face's refusal budget; a valid proof does not,
|
|
3242
|
+
* whatever the answer. */
|
|
3243
|
+
async function handleServiceEndpointAuthority(req, res, ctx, policy) {
|
|
3244
|
+
const door = ctx.serviceAuthority;
|
|
3245
|
+
if (door === undefined)
|
|
3246
|
+
return send(res, 404, { error: "service endpoint authority is not enabled for this space" });
|
|
3247
|
+
if (req.headers.origin !== undefined)
|
|
3248
|
+
return send(res, 403, { error: "browser-origin requests are not served here" });
|
|
3249
|
+
const noStore = { "cache-control": "no-store" };
|
|
3250
|
+
if (req.method === "GET")
|
|
3251
|
+
return send(res, 200, serviceAuthorityInfo(door), noStore);
|
|
3252
|
+
if (req.method !== "POST")
|
|
3253
|
+
return send(res, 405, { error: "GET or POST only" });
|
|
3254
|
+
if (!/^application\/json\b/.test(req.headers["content-type"] ?? ""))
|
|
3255
|
+
return send(res, 415, { error: "content-type must be application/json" });
|
|
3256
|
+
const peer = policy.peerKey(req);
|
|
3257
|
+
const peerThrottled = policy.throttled(ctx, peer);
|
|
3258
|
+
let envelope;
|
|
3259
|
+
try {
|
|
3260
|
+
envelope = parseServiceAuthorityEnvelope(await readJsonBody(req, SERVICE_AUTHORITY_MAX_BYTES));
|
|
3261
|
+
}
|
|
3262
|
+
catch (e) {
|
|
3263
|
+
return sendRequestError(res, e);
|
|
3264
|
+
}
|
|
3265
|
+
const { request, proof } = envelope;
|
|
3266
|
+
const answer = (r) => send(res, r.status, r.body, noStore);
|
|
3267
|
+
if (request.space !== door.space || request.accountPublicKey !== door.accountPublicKey)
|
|
3268
|
+
return answer(serviceAuthorityDenied(request, "account-mismatch"));
|
|
3269
|
+
let caller;
|
|
3270
|
+
try {
|
|
3271
|
+
caller = proof.kind === "idp"
|
|
3272
|
+
? { kind: "idp", owner: deriveOwnerForIdpSubject(ctx.ownerSecret, ctx.bridgeIdp.issuer, (await verifyIdpToken(proof.token, ctx.bridgeIdp)).sub) }
|
|
3273
|
+
: { kind: "service", witness: await verifyServiceProof(proof, request, ctx.issuer, door.space) };
|
|
3274
|
+
}
|
|
3275
|
+
catch (e) {
|
|
3276
|
+
policy.recordFailure(ctx, peer);
|
|
3277
|
+
console.error(`auth-service: refused a service endpoint authority proof: ${e instanceof Error ? e.message : String(e)}`);
|
|
3278
|
+
return answer(serviceAuthorityDenied(request, peerThrottled ? "rate-limited" : "unauthenticated"));
|
|
3279
|
+
}
|
|
3280
|
+
try {
|
|
3281
|
+
return answer(await answerServiceAuthority(door, request, caller));
|
|
3282
|
+
}
|
|
3283
|
+
catch (e) {
|
|
3284
|
+
// After the proof, a register may already be bound: an unexpected failure is an unknown effect.
|
|
3285
|
+
console.error(`auth-service: service endpoint authority ${request.operation} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3286
|
+
return send(res, 503, { error: "service endpoint authority did not complete; the request may be bound, so retain its request identity" }, noStore);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
2991
3289
|
/** The typed manager authority exchange, served on both faces. The face's policy decides whether the
|
|
2992
3290
|
* loopback capability is demanded; on the public face the verified IdP JWT is the proof, and the
|
|
2993
3291
|
* dispatcher reads the caller's scope from the ledger either way. */
|
|
@@ -3194,6 +3492,7 @@ function makePublicHandler(ctx, policy, bundle) {
|
|
|
3194
3492
|
["/exchange", (req, res, c) => handleExchange(req, res, c, policy)],
|
|
3195
3493
|
["/manager-service-authority", (req, res, c) => handleManagerServiceAuthority(req, res, c, policy)],
|
|
3196
3494
|
["/manager-instances", (req, res, c) => handleManagerInstances(req, res, c, policy)],
|
|
3495
|
+
[SERVICE_ENDPOINT_AUTHORITY_PATH, (req, res, c) => handleServiceEndpointAuthority(req, res, c, policy)],
|
|
3197
3496
|
[CHAT_SESSION_PATHS.open, (req, res, c) => handleChatSession(req, res, c, policy, "open")],
|
|
3198
3497
|
[CHAT_SESSION_PATHS.bearer, (req, res, c) => handleChatSession(req, res, c, policy, "bearer")],
|
|
3199
3498
|
[CHAT_SESSION_PATHS.retire, (req, res, c) => handleChatSession(req, res, c, policy, "retire")],
|
|
@@ -3240,16 +3539,16 @@ function makePublicHandler(ctx, policy, bundle) {
|
|
|
3240
3539
|
};
|
|
3241
3540
|
}
|
|
3242
3541
|
/** Read + parse a small JSON body, bounded — the exchange payload is an IdP JWT plus an actor name;
|
|
3243
|
-
* 64 KB clears any sane JWT while keeping the loopback surface un-floodable.
|
|
3244
|
-
|
|
3245
|
-
|
|
3542
|
+
* 64 KB clears any sane JWT while keeping the loopback surface un-floodable. A route with a larger
|
|
3543
|
+
* protocol envelope passes its own bound. */
|
|
3544
|
+
function readJsonBody(req, max = 64 * 1024) {
|
|
3246
3545
|
return new Promise((resolve, reject) => {
|
|
3247
3546
|
let size = 0;
|
|
3248
3547
|
let tooLarge = false;
|
|
3249
3548
|
const chunks = [];
|
|
3250
3549
|
req.on("data", (c) => {
|
|
3251
3550
|
size += c.length;
|
|
3252
|
-
if (size >
|
|
3551
|
+
if (size > max) {
|
|
3253
3552
|
// Stop retaining bytes immediately, but keep draining the request so the client receives
|
|
3254
3553
|
// the explicit 413 JSON response. Destroying the socket hid the server's decision behind
|
|
3255
3554
|
// an undici transport error and made the size-bound smoke unable to prove this guard.
|
|
@@ -3262,7 +3561,7 @@ function readJsonBody(req) {
|
|
|
3262
3561
|
});
|
|
3263
3562
|
req.on("end", () => {
|
|
3264
3563
|
if (tooLarge)
|
|
3265
|
-
return reject(new RequestBodyError(413,
|
|
3564
|
+
return reject(new RequestBodyError(413, `request body too large (maximum ${max} bytes)`));
|
|
3266
3565
|
try {
|
|
3267
3566
|
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3268
3567
|
}
|