@cotal-ai/manager 0.16.0 → 0.18.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/README.md +2 -1
- package/dist/attach-endpoint.d.ts +53 -59
- package/dist/attach-endpoint.d.ts.map +1 -1
- package/dist/attach-endpoint.js +120 -244
- package/dist/attach-endpoint.js.map +1 -1
- package/dist/commands.js +88 -8
- package/dist/commands.js.map +1 -1
- package/dist/console/app.js +68 -30
- package/dist/console/index.html +3 -2
- package/dist/console/session-bundle.js +9736 -0
- package/dist/console-crypto-shim.d.ts +16 -0
- package/dist/console-crypto-shim.d.ts.map +1 -0
- package/dist/console-crypto-shim.js +20 -0
- package/dist/console-crypto-shim.js.map +1 -0
- package/dist/console-session-entry.d.ts +2 -0
- package/dist/console-session-entry.d.ts.map +1 -0
- package/dist/console-session-entry.js +26 -0
- package/dist/console-session-entry.js.map +1 -0
- package/dist/endpoint-evict.d.ts +29 -0
- package/dist/endpoint-evict.d.ts.map +1 -0
- package/dist/endpoint-evict.js +80 -0
- package/dist/endpoint-evict.js.map +1 -0
- package/dist/holder-liveness.d.ts +40 -0
- package/dist/holder-liveness.d.ts.map +1 -0
- package/dist/holder-liveness.js +98 -0
- package/dist/holder-liveness.js.map +1 -0
- package/dist/manager-service-contract.d.ts +132 -0
- package/dist/manager-service-contract.d.ts.map +1 -0
- package/dist/manager-service-contract.js +332 -0
- package/dist/manager-service-contract.js.map +1 -0
- package/dist/manager.d.ts +519 -22
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +2273 -262
- package/dist/manager.js.map +1 -1
- package/dist/reconcile-gate.d.ts +82 -0
- package/dist/reconcile-gate.d.ts.map +1 -0
- package/dist/reconcile-gate.js +118 -0
- package/dist/reconcile-gate.js.map +1 -0
- package/dist/runtime/pty.d.ts.map +1 -1
- package/dist/runtime/pty.js +15 -2
- package/dist/runtime/pty.js.map +1 -1
- package/dist/session/bridge.d.ts +63 -0
- package/dist/session/bridge.d.ts.map +1 -0
- package/dist/session/bridge.js +157 -0
- package/dist/session/bridge.js.map +1 -0
- package/dist/session/establish.d.ts +212 -0
- package/dist/session/establish.d.ts.map +1 -0
- package/dist/session/establish.js +174 -0
- package/dist/session/establish.js.map +1 -0
- package/dist/session/index.d.ts +12 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +15 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/plane.d.ts +120 -0
- package/dist/session/plane.d.ts.map +1 -0
- package/dist/session/plane.js +327 -0
- package/dist/session/plane.js.map +1 -0
- package/dist/static-lifecycle.d.ts +97 -0
- package/dist/static-lifecycle.d.ts.map +1 -0
- package/dist/static-lifecycle.js +262 -0
- package/dist/static-lifecycle.js.map +1 -0
- package/package.json +10 -6
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The manager-side session ESTABLISHMENT (P2 item 6): mint the holder/target-lifecycle/
|
|
3
|
+
* instance+epoch-bound OFFER, and enforce its ONE-USE redemption through the §13.6 composite.
|
|
4
|
+
*
|
|
5
|
+
* Item 6 owns the offer mint + the STATIC redeem enforcement (this module) + the PTY bridge + the
|
|
6
|
+
* CLI/console clients + restart termination. The auth-service USER-MODE redemption handler
|
|
7
|
+
* (callout-minted per-session credentials) and the barrier session reconciler
|
|
8
|
+
* (implementations/auth/src/service.ts:358) are the #29 auth-trigger slice — OUT of item 6. This
|
|
9
|
+
* module exposes both halves through ONE {@link RedemptionSeam} interface: {@link
|
|
10
|
+
* staticRedemptionSeam} (wired) and {@link userModeRedemptionSeam} (a loud refusal until #29 lands
|
|
11
|
+
* — a user-mode attach must fail, never silently degrade to the static path).
|
|
12
|
+
*
|
|
13
|
+
* TARGET BINDING: the §13.6 grant is a CLOSED schema (holder + serving only), so the attached
|
|
14
|
+
* agent's (name, lifecycleUid) is bound MANAGER-SIDE — recorded in the {@link ManagerSessionRegistry}
|
|
15
|
+
* keyed by the fresh sessionId at mint, and re-checked live before the bridge serves (a despawned
|
|
16
|
+
* or replaced target ends the session with `target-despawn`). The holder + serving signature
|
|
17
|
+
* binding already stops a foreign party from redeeming or serving; the registry adds the target
|
|
18
|
+
* lifecycle the pin requires without widening the core artifact.
|
|
19
|
+
*/
|
|
20
|
+
import type { NatsConnection } from "@nats-io/transport-node";
|
|
21
|
+
import { type AnchorResolver, type EpGateState, type LifecycleGatePin, type SessionCredential, type SessionGrant, type SessionLedger, type SessionPresenter } from "@cotal-ai/core";
|
|
22
|
+
/** The attached agent's incarnation coordinate — the "target lifecycle" the offer binds (pin 1). */
|
|
23
|
+
export interface SessionTarget {
|
|
24
|
+
name: string;
|
|
25
|
+
lifecycleUid: string;
|
|
26
|
+
}
|
|
27
|
+
/** The item-6 attach offer: the signed §13.6 grant plus the manager-side target binding. The REPLY
|
|
28
|
+
* to the caller carries the `grant` only (holder-bound, non-bearer — a leak releases nothing);
|
|
29
|
+
* `target` stays server-side (the caller already named it in the attach request). */
|
|
30
|
+
export interface AttachOffer {
|
|
31
|
+
grant: SessionGrant;
|
|
32
|
+
target: SessionTarget;
|
|
33
|
+
}
|
|
34
|
+
export interface MintAttachOfferArgs {
|
|
35
|
+
space: string;
|
|
36
|
+
/** The serving manager incarnation: its lifecycleUid + the current serve-gate epoch (item-3 seam
|
|
37
|
+
* — the §13.1 endpoint gate epoch; a successor incarnation advancing it refuses old sessions). */
|
|
38
|
+
serving: {
|
|
39
|
+
instanceId: string;
|
|
40
|
+
epoch: number;
|
|
41
|
+
};
|
|
42
|
+
/** The redeeming caller's triple, taken from the authenticated attach request (ctx.subject.caller). */
|
|
43
|
+
holder: {
|
|
44
|
+
id: string;
|
|
45
|
+
lifecycleUid: string;
|
|
46
|
+
processEpoch: number;
|
|
47
|
+
};
|
|
48
|
+
/** The attached agent + its incarnation (the target-lifecycle binding, recorded server-side). */
|
|
49
|
+
target: SessionTarget;
|
|
50
|
+
/** The manager's `sessions`-role signer (its own key material; the matching public key is its
|
|
51
|
+
* registered `sessions` trust anchor the redeem/verify path resolves). */
|
|
52
|
+
signer: {
|
|
53
|
+
keyId: string;
|
|
54
|
+
keyPair: {
|
|
55
|
+
sign(input: Uint8Array): Uint8Array;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** Session lifetime (§13.6 live-class; bounds the whole session, not just a redeem window). */
|
|
59
|
+
ttlMs: number;
|
|
60
|
+
/** Max in-flight data frames per direction (bounded flow window). */
|
|
61
|
+
window?: number;
|
|
62
|
+
now?: number;
|
|
63
|
+
}
|
|
64
|
+
/** Mint an attach offer: a §13.6 session grant on the `manager` endpoint bound to the holder triple
|
|
65
|
+
* + serving instance/epoch, paired with the manager-side target binding. Pure — signs + returns;
|
|
66
|
+
* the manager records the offer in its {@link ManagerSessionRegistry}. */
|
|
67
|
+
export declare function mintAttachOffer(args: MintAttachOfferArgs): AttachOffer;
|
|
68
|
+
/** The redemption's serving-gate observation, carried WHOLE rather than as a bare revision.
|
|
69
|
+
*
|
|
70
|
+
* The §13.1 fence has to tell two revision moves apart: a BARRIER (which must refuse) and another
|
|
71
|
+
* sibling mint's identical-bytes commit touch (which must not — every per-session credential
|
|
72
|
+
* serializes on this one gate key, so refusing on contention would fail live sessions for no
|
|
73
|
+
* security reason). A revision alone cannot make that distinction, so the pin carries the gate
|
|
74
|
+
* state its revision came from and the fence compares EVERY field of it. */
|
|
75
|
+
export interface ServingGatePin extends LifecycleGatePin {
|
|
76
|
+
/** The observation the revision came from. Absent ONLY on an open mesh, where nothing is minted,
|
|
77
|
+
* nothing is staged, and there is no gate to fence against; the stage refuses loudly if it is
|
|
78
|
+
* ever missing on an auth mesh rather than staging unfenced. */
|
|
79
|
+
gate?: EpGateState;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The manager's per-session SERVING credential, injected because minting one needs the space auth
|
|
83
|
+
* and the §13.1 endpoint gate, which live on the Manager, not in this module.
|
|
84
|
+
*
|
|
85
|
+
* The four calls are the credential's whole life, in the order §13.6 requires: `mint` produces
|
|
86
|
+
* bytes that are NOT yet authority, `stage` writes the gate-checked §13.1 ledger row that makes
|
|
87
|
+
* them revocable, the redemption's finalize CAS is what promotes them, and `revoke` kills the row
|
|
88
|
+
* by name at the session's terminal. `open` is separate from `mint` on purpose: nothing connects
|
|
89
|
+
* until the session row is `active`.
|
|
90
|
+
*/
|
|
91
|
+
export interface SessionServing {
|
|
92
|
+
/** Mint the per-session serving credential: exact-subject for THIS session only, expiring no
|
|
93
|
+
* later than the grant. Bytes only — no ledger row yet, so this confers nothing durable. */
|
|
94
|
+
mint(grant: SessionGrant): Promise<SessionCredential>;
|
|
95
|
+
/** Stage its §13.1 credential-ledger row into the serving instance's revocation family, fenced on
|
|
96
|
+
* `pin` — the gate observed during THIS redemption, which stays the authority: the commit CASes
|
|
97
|
+
* on its revision, and a lost CAS is refused unless the gate is still identical in every field
|
|
98
|
+
* but the revision. MUST throw if the gate moved — that loss is the lifecycle fence, and a throw
|
|
99
|
+
* here refuses the whole redemption. */
|
|
100
|
+
stage(grant: SessionGrant, cred: SessionCredential, pin: ServingGatePin): Promise<void>;
|
|
101
|
+
/** Open the serving connection for ONE session with that credential. MUST fail loud: there is no
|
|
102
|
+
* shared connection to fall back to, and serving a session without its own credential is exactly
|
|
103
|
+
* the standing-writer shape this design removes. */
|
|
104
|
+
open(cred: SessionCredential): Promise<NatsConnection>;
|
|
105
|
+
/** Revoke the credential by id, idempotently (§13.6 makes the sweep retry until the mark sticks).
|
|
106
|
+
*
|
|
107
|
+
* RESIDUAL, NAMED: this marks the §13.1 ledger row revoked; it does NOT evict a live connection.
|
|
108
|
+
* The session's own teardown closes its connection first, so in the ordinary path the credential
|
|
109
|
+
* has no connection left to use — but if that close fails, the JWT stays broker-valid until its
|
|
110
|
+
* TTL (bounded by the session exp). Cluster-verified eviction of these holders belongs to the
|
|
111
|
+
* §13.1 takeover barrier, which enumerates the family. So: do not read this as immediate broker
|
|
112
|
+
* death on session close. */
|
|
113
|
+
revoke(credentialId: string): Promise<void>;
|
|
114
|
+
/** LEADER-SERVED observation of the serving instance's §13.1 issuance gate, returned as the pin
|
|
115
|
+
* {@link stage} is CASed against. Real, not a placeholder: it is what makes the stage a fence. */
|
|
116
|
+
observeGate(endpoint: string, instanceId: string): Promise<ServingGatePin>;
|
|
117
|
+
}
|
|
118
|
+
/** The one interface both modes present. `redeem` VERIFIES the presented grant (signature/anchor/
|
|
119
|
+
* currency) then enforces the one-use CAS + presenter-equality; it THROWS on any refusal (a second
|
|
120
|
+
* redeem, a foreign presenter, an expired/forged grant) and returns the sessionId on success.
|
|
121
|
+
* `serving` then hands the SERVING party its own credential — a separate, authenticated retrieval
|
|
122
|
+
* (§13.6 per-party release: no private material crosses between the two parties, so it is never
|
|
123
|
+
* folded into the redemption answer). */
|
|
124
|
+
export interface RedemptionSeam {
|
|
125
|
+
redeem(grant: SessionGrant, presenter: SessionPresenter): Promise<{
|
|
126
|
+
sessionId: string;
|
|
127
|
+
}>;
|
|
128
|
+
serving(sessionId: string, presenter: {
|
|
129
|
+
endpoint: string;
|
|
130
|
+
instanceId: string;
|
|
131
|
+
epoch: number;
|
|
132
|
+
}): Promise<SessionCredential>;
|
|
133
|
+
}
|
|
134
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
135
|
+
/** What {@link staticRedemptionSeam} reads from the manager's durable authority. In production the
|
|
136
|
+
* ledger + gate/epoch reads come from the auth store (leader-served); the smoke supplies an
|
|
137
|
+
* in-memory faithful ledger. */
|
|
138
|
+
export interface StaticRedemptionDeps {
|
|
139
|
+
space: string;
|
|
140
|
+
resolveAnchor: AnchorResolver;
|
|
141
|
+
ledger: SessionLedger;
|
|
142
|
+
/** The per-session serving credential's mint/stage/open/revoke seam (see {@link SessionServing}). */
|
|
143
|
+
serving: SessionServing;
|
|
144
|
+
holderProcessEpoch(holder: {
|
|
145
|
+
id: string;
|
|
146
|
+
lifecycleUid: string;
|
|
147
|
+
}): MaybePromise<number | undefined>;
|
|
148
|
+
servingEpoch(endpoint: string, instanceId: string): MaybePromise<number | undefined>;
|
|
149
|
+
observeHolderGate(holder: {
|
|
150
|
+
id: string;
|
|
151
|
+
lifecycleUid: string;
|
|
152
|
+
}): MaybePromise<LifecycleGatePin>;
|
|
153
|
+
observeServingGate(endpoint: string, instanceId: string): MaybePromise<ServingGatePin>;
|
|
154
|
+
now?(): number;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The STATIC redeem seam (item 6's wired path): the manager runs the §13.6 redemption — verify the
|
|
158
|
+
* signed grant, then `redeemSession` (the one-use `issuing` create-CAS + presenter-equality + the
|
|
159
|
+
* gate-pinned stage + the finalize's fresh epoch re-checks) — and, for the SERVING half, mints,
|
|
160
|
+
* stages, releases and revokes a real per-session credential through {@link SessionServing}.
|
|
161
|
+
*
|
|
162
|
+
* WHAT EACH HALF ACTUALLY GETS, stated exactly, because the two halves are NOT symmetric here and a
|
|
163
|
+
* comment claiming they are would be worse than the asymmetry:
|
|
164
|
+
*
|
|
165
|
+
* - SERVING half (this manager's own): a real `session-serving` credential, exact-subject
|
|
166
|
+
* (`eps.<endpoint>.<sessionId>.<epoch>.{in,out}`), TTL-bound to the session, staged into this
|
|
167
|
+
* instance's §13.1 `epcred.<endpoint>.<instanceId>` family under the open-and-commit fence, and
|
|
168
|
+
* revoked BY NAME at the session's terminal. It replaces a STANDING wildcard credential that
|
|
169
|
+
* reached every live session's bytes at its epoch (SPEC 13.9:2526).
|
|
170
|
+
* - CALLER half: NOT minted, staged, or revoked here, and `releaseCredential` returns a MARKER with
|
|
171
|
+
* no usable bytes for it. The caller's `session-caller` JWT is minted OUT OF BAND from the local
|
|
172
|
+
* space seed — by the console establisher and by CLI attach — after redemption.
|
|
173
|
+
*
|
|
174
|
+
* SPEC 13.6 PAIR-REVOKE IS NOT IMPLEMENTED FOR THE CALLER HALF. Stated exactly, because the
|
|
175
|
+
* difference between this and pair-revoke is the whole residual: that JWT is not written into the
|
|
176
|
+
* session lineage, is not named by the session row's credential material, and is untouched by
|
|
177
|
+
* session close and by takeover reconcile. It dies by TTL alone, at a 24h ceiling
|
|
178
|
+
* (`SESSION_GRANT_MAX_TTL_MS`).
|
|
179
|
+
*
|
|
180
|
+
* Why the window is tolerable for THIS static slice, as reviewed: the caller grant is
|
|
181
|
+
* exact-pinned to one `(endpoint, sessionId, epoch)`, so it can reach neither a sibling session
|
|
182
|
+
* nor a successor epoch; and once the serving half is revoked and its connection closed there is
|
|
183
|
+
* no counterparty on those two subjects. What survives is a broker-valid connection, not reach
|
|
184
|
+
* into another session. Closing it properly means minting the caller half inside redemption,
|
|
185
|
+
* which needs the user-mode (#29) callout exchange. Until that lands, do not describe any part of
|
|
186
|
+
* this as pair teardown.
|
|
187
|
+
* - `revokeCredential` is therefore ONE-SIDED by construction: it revokes an id this seam minted
|
|
188
|
+
* and is an explicit no-op for the caller id, rather than silently appearing to revoke both.
|
|
189
|
+
*/
|
|
190
|
+
export declare function staticRedemptionSeam(deps: StaticRedemptionDeps): RedemptionSeam;
|
|
191
|
+
/** The USER-MODE redeem seam: UNWIRED (the #29 auth-trigger slice owns the callout-minted
|
|
192
|
+
* per-session credential path). It REFUSES LOUD, naming the path — a user-mode attach fails rather
|
|
193
|
+
* than degrading to the static seam (item-6 binding no-fallback rule). */
|
|
194
|
+
export declare function userModeRedemptionSeam(): RedemptionSeam;
|
|
195
|
+
/** The manager's in-memory session registry: the sessionId → (target lifecycle, offer) binding the
|
|
196
|
+
* §13.6 grant cannot carry. The serve path looks the target up here and re-checks its liveness
|
|
197
|
+
* before bridging, so a despawned/replaced target ends the session (`target-despawn`). */
|
|
198
|
+
export declare class ManagerSessionRegistry {
|
|
199
|
+
#private;
|
|
200
|
+
/** Record a freshly minted offer. Throws on a sessionId collision (a fresh unguessable id never
|
|
201
|
+
* repeats — a repeat is a mint bug, never silently overwritten). */
|
|
202
|
+
record(offer: AttachOffer): void;
|
|
203
|
+
/** The target bound to a sessionId, or undefined if unknown/removed. */
|
|
204
|
+
target(sessionId: string): SessionTarget | undefined;
|
|
205
|
+
/** Drop a session (redeem-refused, closed, expired, or target gone). Idempotent. */
|
|
206
|
+
remove(sessionId: string): void;
|
|
207
|
+
/** Every sessionId whose target is a given agent incarnation — the set a despawn/restart must end. */
|
|
208
|
+
forTarget(name: string, lifecycleUid: string): string[];
|
|
209
|
+
get size(): number;
|
|
210
|
+
}
|
|
211
|
+
export {};
|
|
212
|
+
//# sourceMappingURL=establish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"establish.d.ts","sourceRoot":"","sources":["../../src/session/establish.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { EpEnvelopeError, mintSessionGrant, redeemSession, retrieveServingCredential, verifySessionGrant, } from "@cotal-ai/core";
|
|
2
|
+
import { MANAGER_ENDPOINT } from "../manager-service-contract.js";
|
|
3
|
+
/** Mint an attach offer: a §13.6 session grant on the `manager` endpoint bound to the holder triple
|
|
4
|
+
* + serving instance/epoch, paired with the manager-side target binding. Pure — signs + returns;
|
|
5
|
+
* the manager records the offer in its {@link ManagerSessionRegistry}. */
|
|
6
|
+
export function mintAttachOffer(args) {
|
|
7
|
+
const grant = mintSessionGrant({
|
|
8
|
+
space: args.space,
|
|
9
|
+
endpoint: MANAGER_ENDPOINT,
|
|
10
|
+
holder: args.holder,
|
|
11
|
+
serving: args.serving,
|
|
12
|
+
ttlMs: args.ttlMs,
|
|
13
|
+
...(args.window !== undefined ? { window: args.window } : {}),
|
|
14
|
+
issuerKeyId: args.signer.keyId,
|
|
15
|
+
...(args.now !== undefined ? { now: args.now } : {}),
|
|
16
|
+
}, args.signer.keyPair);
|
|
17
|
+
return { grant, target: { name: args.target.name, lifecycleUid: args.target.lifecycleUid } };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The STATIC redeem seam (item 6's wired path): the manager runs the §13.6 redemption — verify the
|
|
21
|
+
* signed grant, then `redeemSession` (the one-use `issuing` create-CAS + presenter-equality + the
|
|
22
|
+
* gate-pinned stage + the finalize's fresh epoch re-checks) — and, for the SERVING half, mints,
|
|
23
|
+
* stages, releases and revokes a real per-session credential through {@link SessionServing}.
|
|
24
|
+
*
|
|
25
|
+
* WHAT EACH HALF ACTUALLY GETS, stated exactly, because the two halves are NOT symmetric here and a
|
|
26
|
+
* comment claiming they are would be worse than the asymmetry:
|
|
27
|
+
*
|
|
28
|
+
* - SERVING half (this manager's own): a real `session-serving` credential, exact-subject
|
|
29
|
+
* (`eps.<endpoint>.<sessionId>.<epoch>.{in,out}`), TTL-bound to the session, staged into this
|
|
30
|
+
* instance's §13.1 `epcred.<endpoint>.<instanceId>` family under the open-and-commit fence, and
|
|
31
|
+
* revoked BY NAME at the session's terminal. It replaces a STANDING wildcard credential that
|
|
32
|
+
* reached every live session's bytes at its epoch (SPEC 13.9:2526).
|
|
33
|
+
* - CALLER half: NOT minted, staged, or revoked here, and `releaseCredential` returns a MARKER with
|
|
34
|
+
* no usable bytes for it. The caller's `session-caller` JWT is minted OUT OF BAND from the local
|
|
35
|
+
* space seed — by the console establisher and by CLI attach — after redemption.
|
|
36
|
+
*
|
|
37
|
+
* SPEC 13.6 PAIR-REVOKE IS NOT IMPLEMENTED FOR THE CALLER HALF. Stated exactly, because the
|
|
38
|
+
* difference between this and pair-revoke is the whole residual: that JWT is not written into the
|
|
39
|
+
* session lineage, is not named by the session row's credential material, and is untouched by
|
|
40
|
+
* session close and by takeover reconcile. It dies by TTL alone, at a 24h ceiling
|
|
41
|
+
* (`SESSION_GRANT_MAX_TTL_MS`).
|
|
42
|
+
*
|
|
43
|
+
* Why the window is tolerable for THIS static slice, as reviewed: the caller grant is
|
|
44
|
+
* exact-pinned to one `(endpoint, sessionId, epoch)`, so it can reach neither a sibling session
|
|
45
|
+
* nor a successor epoch; and once the serving half is revoked and its connection closed there is
|
|
46
|
+
* no counterparty on those two subjects. What survives is a broker-valid connection, not reach
|
|
47
|
+
* into another session. Closing it properly means minting the caller half inside redemption,
|
|
48
|
+
* which needs the user-mode (#29) callout exchange. Until that lands, do not describe any part of
|
|
49
|
+
* this as pair teardown.
|
|
50
|
+
* - `revokeCredential` is therefore ONE-SIDED by construction: it revokes an id this seam minted
|
|
51
|
+
* and is an explicit no-op for the caller id, rather than silently appearing to revoke both.
|
|
52
|
+
*/
|
|
53
|
+
export function staticRedemptionSeam(deps) {
|
|
54
|
+
// The minted-but-unreleased serving credential, held between `allocateCredentialIds` and
|
|
55
|
+
// `releaseCredential`. NAMED RESIDUAL: the JWT exists before the one-use create-CAS is won,
|
|
56
|
+
// because the credential id the `issuing` row must name IS its digest. If the create loses, the
|
|
57
|
+
// bytes are dropped here having never been staged, released, or connected with — they reach no
|
|
58
|
+
// other party and no ledger row was written for them.
|
|
59
|
+
const minted = new Map();
|
|
60
|
+
const callerId = (grant) => `${grant.sessionId}.c`;
|
|
61
|
+
const marker = async (sessionId, credentialId) => {
|
|
62
|
+
const row = await deps.ledger.read(sessionId);
|
|
63
|
+
if (!row)
|
|
64
|
+
throw new EpEnvelopeError("failed-precondition", `no session row for ${sessionId}; nothing releases without its authority row (SPEC 13.6)`);
|
|
65
|
+
return { id: credentialId, creds: "", exp: row.exp };
|
|
66
|
+
};
|
|
67
|
+
// The serving pin core observed for THIS redemption, kept so the stage fences on the WHOLE
|
|
68
|
+
// observation rather than the bare {key, revision} core's hook type carries. One seam is built per
|
|
69
|
+
// establishment, so this holds exactly one redemption's pin and cannot be crossed with another's.
|
|
70
|
+
let servingPin;
|
|
71
|
+
const hooks = {
|
|
72
|
+
ledger: deps.ledger,
|
|
73
|
+
holderProcessEpoch: (h) => deps.holderProcessEpoch(h),
|
|
74
|
+
servingEpoch: (e, i) => deps.servingEpoch(e, i),
|
|
75
|
+
observeHolderGate: (h) => deps.observeHolderGate(h),
|
|
76
|
+
observeServingGate: async (e, i) => {
|
|
77
|
+
servingPin = await deps.observeServingGate(e, i);
|
|
78
|
+
return servingPin;
|
|
79
|
+
},
|
|
80
|
+
allocateCredentialIds: async (grant) => {
|
|
81
|
+
const cred = await deps.serving.mint(grant);
|
|
82
|
+
minted.set(grant.sessionId, cred);
|
|
83
|
+
return { credCaller: callerId(grant), credServing: cred.id };
|
|
84
|
+
},
|
|
85
|
+
// The §13.1 fence for the half this manager owns: stage the serving credential's ledger row
|
|
86
|
+
// REVISION-PINNED to the serving gate observed moments ago. A barrier that moved the gate makes
|
|
87
|
+
// this LOSE and throw, and redeemSession turns that into a refusal that releases nothing.
|
|
88
|
+
stagePair: async (grant, ids, pins) => {
|
|
89
|
+
const cred = minted.get(grant.sessionId);
|
|
90
|
+
if (!cred || cred.id !== ids.credServing)
|
|
91
|
+
throw new EpEnvelopeError("internal", `no minted serving credential for session ${grant.sessionId}; the stage cannot pin a credential that was never minted (SPEC 13.6)`);
|
|
92
|
+
// The pin handed to the stage MUST be the one this seam observed. Core passes the observation
|
|
93
|
+
// through, so this is an identity re-check, not a conversion: if it ever failed we would be
|
|
94
|
+
// fencing against a gate nobody in this redemption read, which is worse than not staging.
|
|
95
|
+
if (!servingPin || servingPin.revision !== pins.serving.revision || servingPin.key !== pins.serving.key)
|
|
96
|
+
throw new EpEnvelopeError("internal", `the serving gate pin staged against is not the one this redemption observed for session ${grant.sessionId} (SPEC 13.1)`);
|
|
97
|
+
await deps.serving.stage(grant, cred, servingPin);
|
|
98
|
+
},
|
|
99
|
+
releaseCredential: async (sessionId, credentialId) => {
|
|
100
|
+
const cred = minted.get(sessionId);
|
|
101
|
+
// Idempotent for the row's life (§13.6 lost-response retry): the SAME bytes, never a re-mint.
|
|
102
|
+
if (cred && cred.id === credentialId)
|
|
103
|
+
return cred;
|
|
104
|
+
return marker(sessionId, credentialId); // the caller half: no bytes to hand back (see doc)
|
|
105
|
+
},
|
|
106
|
+
revokeCredential: async (id) => {
|
|
107
|
+
for (const [sessionId, cred] of minted) {
|
|
108
|
+
if (cred.id !== id)
|
|
109
|
+
continue;
|
|
110
|
+
await deps.serving.revoke(id);
|
|
111
|
+
minted.delete(sessionId);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Not ours: the caller id, which this seam never minted and therefore cannot revoke.
|
|
115
|
+
},
|
|
116
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
async redeem(grant, presenter) {
|
|
120
|
+
const now = deps.now?.() ?? Date.now();
|
|
121
|
+
const verified = await verifySessionGrant(grant, { space: deps.space, resolveAnchor: deps.resolveAnchor, now });
|
|
122
|
+
try {
|
|
123
|
+
await redeemSession(verified, presenter, hooks); // one-use CAS + presenter-equality (throws on refuse)
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
minted.delete(verified.sessionId); // a refused redemption keeps no minted bytes
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
return { sessionId: verified.sessionId };
|
|
130
|
+
},
|
|
131
|
+
serving: (sessionId, presenter) => retrieveServingCredential(sessionId, presenter, hooks),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** The USER-MODE redeem seam: UNWIRED (the #29 auth-trigger slice owns the callout-minted
|
|
135
|
+
* per-session credential path). It REFUSES LOUD, naming the path — a user-mode attach fails rather
|
|
136
|
+
* than degrading to the static seam (item-6 binding no-fallback rule). */
|
|
137
|
+
export function userModeRedemptionSeam() {
|
|
138
|
+
const refuse = () => Promise.reject(new EpEnvelopeError("unimplemented", "user-mode session redemption (callout-minted per-session credentials) is not wired in this build; " +
|
|
139
|
+
"it is the #29 auth-trigger slice. A user-mode attach fails loud rather than degrading (SPEC 13.6, item-6 no-fallback)."));
|
|
140
|
+
return { redeem: refuse, serving: refuse };
|
|
141
|
+
}
|
|
142
|
+
/** The manager's in-memory session registry: the sessionId → (target lifecycle, offer) binding the
|
|
143
|
+
* §13.6 grant cannot carry. The serve path looks the target up here and re-checks its liveness
|
|
144
|
+
* before bridging, so a despawned/replaced target ends the session (`target-despawn`). */
|
|
145
|
+
export class ManagerSessionRegistry {
|
|
146
|
+
#byId = new Map();
|
|
147
|
+
/** Record a freshly minted offer. Throws on a sessionId collision (a fresh unguessable id never
|
|
148
|
+
* repeats — a repeat is a mint bug, never silently overwritten). */
|
|
149
|
+
record(offer) {
|
|
150
|
+
if (this.#byId.has(offer.grant.sessionId))
|
|
151
|
+
throw new EpEnvelopeError("already-exists", `session ${offer.grant.sessionId} is already recorded; a fresh sessionId never repeats (SPEC 13.6)`);
|
|
152
|
+
this.#byId.set(offer.grant.sessionId, { target: offer.target, grant: offer.grant });
|
|
153
|
+
}
|
|
154
|
+
/** The target bound to a sessionId, or undefined if unknown/removed. */
|
|
155
|
+
target(sessionId) {
|
|
156
|
+
return this.#byId.get(sessionId)?.target;
|
|
157
|
+
}
|
|
158
|
+
/** Drop a session (redeem-refused, closed, expired, or target gone). Idempotent. */
|
|
159
|
+
remove(sessionId) {
|
|
160
|
+
this.#byId.delete(sessionId);
|
|
161
|
+
}
|
|
162
|
+
/** Every sessionId whose target is a given agent incarnation — the set a despawn/restart must end. */
|
|
163
|
+
forTarget(name, lifecycleUid) {
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const [id, e] of this.#byId)
|
|
166
|
+
if (e.target.name === name && e.target.lifecycleUid === lifecycleUid)
|
|
167
|
+
out.push(id);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
get size() {
|
|
171
|
+
return this.#byId.size;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=establish.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"establish.js","sourceRoot":"","sources":["../../src/session/establish.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,yBAAyB,EACzB,kBAAkB,GASnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAmClE;;2EAE2E;AAC3E,MAAM,UAAU,eAAe,CAAC,IAAyB;IACvD,MAAM,KAAK,GAAG,gBAAgB,CAC5B;QACE,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,gBAAgB;QAC1B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;QAC9B,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACrD,EACD,IAAI,CAAC,MAAM,CAAC,OAAO,CACpB,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,CAAC;AAC/F,CAAC;AAqFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAA0B;IAC7D,yFAAyF;IACzF,4FAA4F;IAC5F,gGAAgG;IAChG,+FAA+F;IAC/F,sDAAsD;IACtD,MAAM,MAAM,GAAG,IAAI,GAAG,EAA6B,CAAC;IACpD,MAAM,QAAQ,GAAG,CAAC,KAAmB,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,IAAI,CAAC;IACjE,MAAM,MAAM,GAAG,KAAK,EAAE,SAAiB,EAAE,YAAoB,EAA8B,EAAE;QAC3F,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,eAAe,CAAC,qBAAqB,EAAE,sBAAsB,SAAS,0DAA0D,CAAC,CAAC;QACtJ,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC;IACvD,CAAC,CAAC;IACF,2FAA2F;IAC3F,mGAAmG;IACnG,kGAAkG;IAClG,IAAI,UAAsC,CAAC;IAC3C,MAAM,KAAK,GAA2B;QACpC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,kBAAkB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;QACrD,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/C,iBAAiB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC;QACnD,kBAAkB,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;YACjC,UAAU,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACjD,OAAO,UAAU,CAAC;QACpB,CAAC;QACD,qBAAqB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACrC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC5C,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAClC,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;QAC/D,CAAC;QACD,4FAA4F;QAC5F,gGAAgG;QAChG,0FAA0F;QAC1F,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACpC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YACzC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,GAAG,CAAC,WAAW;gBACtC,MAAM,IAAI,eAAe,CAAC,UAAU,EAAE,4CAA4C,KAAK,CAAC,SAAS,uEAAuE,CAAC,CAAC;YAC5K,8FAA8F;YAC9F,4FAA4F;YAC5F,0FAA0F;YAC1F,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,QAAQ,KAAK,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,UAAU,CAAC,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG;gBACrG,MAAM,IAAI,eAAe,CAAC,UAAU,EAAE,2FAA2F,KAAK,CAAC,SAAS,cAAc,CAAC,CAAC;YAClK,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;QACpD,CAAC;QACD,iBAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE;YACnD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACnC,8FAA8F;YAC9F,IAAI,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,YAAY;gBAAE,OAAO,IAAI,CAAC;YAClD,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,mDAAmD;QAC7F,CAAC;QACD,gBAAgB,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;YAC7B,KAAK,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;gBACvC,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE;oBAAE,SAAS;gBAC7B,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBAC9B,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,qFAAqF;QACvF,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvC,CAAC;IACF,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS;YAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE,CAAC,CAAC;YAChH,IAAI,CAAC;gBACH,MAAM,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,sDAAsD;YACzG,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,6CAA6C;gBAChF,MAAM,CAAC,CAAC;YACV,CAAC;YACD,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;QAC3C,CAAC;QACD,OAAO,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,CAAC,yBAAyB,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,CAAC;KAC1F,CAAC;AACJ,CAAC;AAED;;2EAE2E;AAC3E,MAAM,UAAU,sBAAsB;IACpC,MAAM,MAAM,GAAG,GAAmB,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,eAAe,CACrE,eAAe,EACf,oGAAoG;QAClG,wHAAwH,CAC3H,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7C,CAAC;AAUD;;2FAE2F;AAC3F,MAAM,OAAO,sBAAsB;IACjC,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;IAExC;yEACqE;IACrE,MAAM,CAAC,KAAkB;QACvB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC;YACvC,MAAM,IAAI,eAAe,CAAC,gBAAgB,EAAE,WAAW,KAAK,CAAC,KAAK,CAAC,SAAS,mEAAmE,CAAC,CAAC;QACnJ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,wEAAwE;IACxE,MAAM,CAAC,SAAiB;QACtB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC3C,CAAC;IAED,oFAAoF;IACpF,MAAM,CAAC,SAAiB;QACtB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IAED,sGAAsG;IACtG,SAAS,CAAC,IAAY,EAAE,YAAoB;QAC1C,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,YAAY,KAAK,YAAY;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrH,OAAO,GAAG,CAAC;IACb,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 item 6 — the manager's mesh-attach session plane: the offer mint + redeem enforcement, the
|
|
3
|
+
* PTY ↔ rail bridge, and the application framing over the core §13.6 session composite. The
|
|
4
|
+
* generic composite (grant/redeem/rails/ledger) lives in `@cotal-ai/core` +
|
|
5
|
+
* `implementations/auth`; this module is the manager-side CONSUMER that replaces the loopback
|
|
6
|
+
* `ws://127.0.0.1` attach face.
|
|
7
|
+
*/
|
|
8
|
+
export { mintAttachOffer, staticRedemptionSeam, userModeRedemptionSeam, ManagerSessionRegistry, type AttachOffer, type MintAttachOfferArgs, type SessionTarget, type RedemptionSeam, type SessionServing, type StaticRedemptionDeps, } from "./establish.js";
|
|
9
|
+
export { serveSessionBridge, type ServeSessionBridgeOpts, type SessionBridge, type AttachEndReason } from "./bridge.js";
|
|
10
|
+
export { ManagerSessionPlane, kvManagerSessionLedger, openSessionLedgerKv, MAX_LIVE_SESSIONS_DEFAULT, type ManagerSessionPlaneDeps, } from "./plane.js";
|
|
11
|
+
export { encodeTerminalData, terminalFrameBytes, decodeTerminalFrame, type TerminalFrame } from "@cotal-ai/core";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AACxH,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,uBAAuB,GAC7B,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 item 6 — the manager's mesh-attach session plane: the offer mint + redeem enforcement, the
|
|
3
|
+
* PTY ↔ rail bridge, and the application framing over the core §13.6 session composite. The
|
|
4
|
+
* generic composite (grant/redeem/rails/ledger) lives in `@cotal-ai/core` +
|
|
5
|
+
* `implementations/auth`; this module is the manager-side CONSUMER that replaces the loopback
|
|
6
|
+
* `ws://127.0.0.1` attach face.
|
|
7
|
+
*/
|
|
8
|
+
export { mintAttachOffer, staticRedemptionSeam, userModeRedemptionSeam, ManagerSessionRegistry, } from "./establish.js";
|
|
9
|
+
export { serveSessionBridge } from "./bridge.js";
|
|
10
|
+
export { ManagerSessionPlane, kvManagerSessionLedger, openSessionLedgerKv, MAX_LIVE_SESSIONS_DEFAULT, } from "./plane.js";
|
|
11
|
+
// The terminal-session frame codec is NORMATIVE WIRE and lives in core (the §13.6 terminal-session
|
|
12
|
+
// profile); re-exported here for the manager's own consumers/smokes. The CLI + console import it
|
|
13
|
+
// straight from @cotal-ai/core (implementations cannot import each other).
|
|
14
|
+
export { encodeTerminalData, terminalFrameBytes, decodeTerminalFrame } from "@cotal-ai/core";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,GAOvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAyE,MAAM,aAAa,CAAC;AACxH,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,yBAAyB,GAE1B,MAAM,YAAY,CAAC;AACpB,mGAAmG;AACnG,iGAAiG;AACjG,2EAA2E;AAC3E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,mBAAmB,EAAsB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { KV } from "@nats-io/kv";
|
|
2
|
+
import type { NatsConnection } from "@nats-io/transport-node";
|
|
3
|
+
import { type AnchorResolver, type AttachSession, type SessionGrant, type SessionLedger } from "@cotal-ai/core";
|
|
4
|
+
import { type SessionServing, type SessionTarget } from "./establish.js";
|
|
5
|
+
import type { AttachEndReason } from "./bridge.js";
|
|
6
|
+
/** A manager-local {@link SessionLedger} over `session.<id>` rows in the DEDICATED sessions bucket
|
|
7
|
+
* KV (never the auth bucket — §13.9 subject-blindness confinement). Drives the SAME core row/key
|
|
8
|
+
* types as implementations/auth's ledger with the manager's own KV CAS: the
|
|
9
|
+
* one-use is `createIssuing`'s create-only CAS; finalize/terminal/revoke are revision-pinned
|
|
10
|
+
* updates (a lost CAS returns false / retries on the next sweep, never a silent overwrite). */
|
|
11
|
+
export declare function kvManagerSessionLedger(kv: KV): SessionLedger;
|
|
12
|
+
/** The default global live-session ceiling. Configurable per manager
|
|
13
|
+
* ({@link ManagerSessionPlaneDeps.maxSessions}) because the right number is deployment-shaped: the
|
|
14
|
+
* browser console opens a session PER PANE, so a dashboard over a large mesh legitimately holds
|
|
15
|
+
* many at once, and a ceiling low enough to feel like a bound would break it. */
|
|
16
|
+
export declare const MAX_LIVE_SESSIONS_DEFAULT = 64;
|
|
17
|
+
export interface ManagerSessionPlaneDeps {
|
|
18
|
+
space: string;
|
|
19
|
+
/** The serving manager incarnation — instanceId = the managerLifecycleUid, epoch = the serve grant epoch. */
|
|
20
|
+
serving: {
|
|
21
|
+
instanceId: string;
|
|
22
|
+
epoch: number;
|
|
23
|
+
};
|
|
24
|
+
/** The manager's `sessions`-role signer + the resolver that returns its matching anchor (the
|
|
25
|
+
* manager self-signs and self-verifies its own offers). */
|
|
26
|
+
signer: {
|
|
27
|
+
keyId: string;
|
|
28
|
+
keyPair: {
|
|
29
|
+
sign(input: Uint8Array): Uint8Array;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
resolveAnchor: AnchorResolver;
|
|
33
|
+
/** The DEDICATED sessions-bucket KV holding the `session.<id>` ledger rows (never the auth bucket). */
|
|
34
|
+
ledgerKv: KV;
|
|
35
|
+
/** The per-session SERVING credential seam: mint, gate-checked stage, connection open, revoke.
|
|
36
|
+
* Injected because minting needs the space auth + the §13.1 gate, which live on the Manager. */
|
|
37
|
+
servingCredential: SessionServing;
|
|
38
|
+
/** Session lifetime (§13.6 live-class) + the bounded flow window. */
|
|
39
|
+
ttlMs: number;
|
|
40
|
+
window?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Global ceiling on concurrently live sessions; defaults to {@link MAX_LIVE_SESSIONS_DEFAULT}.
|
|
43
|
+
*
|
|
44
|
+
* Redemption is caller-triggered and each session now mints a credential and opens a connection,
|
|
45
|
+
* so without a ceiling an authorized caller could drive both without bound. This is deliberately
|
|
46
|
+
* the plane's OWN bound rather than an inherited one: whatever route authz sits in front of
|
|
47
|
+
* establishment, the process-level resource limit is a number here that can be executed against.
|
|
48
|
+
*
|
|
49
|
+
* RESIDUAL, NAMED: the ceiling is GLOBAL, not per caller. It protects the process, which is what
|
|
50
|
+
* it is for, but it does not stop one authorized operator from exhausting it and denying sessions
|
|
51
|
+
* to another. That was accepted on the grounds that only authorized operators reach redemption at
|
|
52
|
+
* all — the attach face's console-token gate refuses before the establisher runs. If that gate is
|
|
53
|
+
* ever relaxed, weakened, or made optional, this scoping assumption goes with it and the ceiling
|
|
54
|
+
* needs a per-caller dimension.
|
|
55
|
+
*/
|
|
56
|
+
maxSessions?: number;
|
|
57
|
+
now?(): number;
|
|
58
|
+
}
|
|
59
|
+
export declare class ManagerSessionPlane {
|
|
60
|
+
#private;
|
|
61
|
+
constructor(deps: ManagerSessionPlaneDeps);
|
|
62
|
+
/** Mint an offer for the authenticated attach caller, enforce its one-use redemption, and stand
|
|
63
|
+
* up the PTY bridge — all atomically. Returns the HOLDER-BOUND grant (the attach reply): no URL,
|
|
64
|
+
* and it must never be logged. Throws on a redeem refusal (a foreign presenter, an already-live
|
|
65
|
+
* session, a stale serving epoch). */
|
|
66
|
+
establishAttach(caller: {
|
|
67
|
+
owner: string;
|
|
68
|
+
actor: string;
|
|
69
|
+
uid: string;
|
|
70
|
+
}, target: SessionTarget, session: AttachSession,
|
|
71
|
+
/** A slot already claimed by an establisher in front of this one ({@link claimSlot}), so the
|
|
72
|
+
* reservation covers ITS side effects too (the PTY attach above all). Omitted = claim here. */
|
|
73
|
+
claimed?: {
|
|
74
|
+
release(): void;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
grant: SessionGrant;
|
|
77
|
+
}>;
|
|
78
|
+
/** End every session bound to a target incarnation, with a distinct reason — the despawn/restart
|
|
79
|
+
* hook (the successor incarnation's differing epoch already refuses old-epoch redemptions; this
|
|
80
|
+
* proactively tears the live bridges down and surfaces the reason to each client). The bridge's
|
|
81
|
+
* own `onEnd` drives {@link #teardown}, so the connection and credential go with it. */
|
|
82
|
+
endForTarget(name: string, lifecycleUid: string, reason: AttachEndReason): void;
|
|
83
|
+
/** End all live sessions (manager stop / drain). */
|
|
84
|
+
endAll(reason: AttachEndReason): void;
|
|
85
|
+
/** Await every in-flight teardown (manager stop): the bridges' `onEnd` teardown is async, so a
|
|
86
|
+
* synchronous `endAll` alone would let the process exit with connections still draining. */
|
|
87
|
+
drain(reason: AttachEndReason): Promise<void>;
|
|
88
|
+
get liveSessions(): number;
|
|
89
|
+
/** The configured global ceiling (surfaced so an operator-facing status can report it). */
|
|
90
|
+
get maxSessions(): number;
|
|
91
|
+
/**
|
|
92
|
+
* CLAIM a session slot, or refuse loudly. ONE implementation, called by {@link establishAttach}
|
|
93
|
+
* and by every establisher in front of it, so the two cannot drift into disagreeing about
|
|
94
|
+
* capacity. It must run before ANY of: minting the offer, redeeming it, minting a per-session
|
|
95
|
+
* credential on either side, opening a connection, or attaching the target's PTY.
|
|
96
|
+
*
|
|
97
|
+
* THIS RESERVES, IT DOES NOT MERELY CHECK. A bare read of the live count would be check-then-act:
|
|
98
|
+
* establishment awaits a mint, a redemption and a connection before the session becomes live, so
|
|
99
|
+
* two concurrent establishes at `max - 1` would both observe room, both mint credentials, and
|
|
100
|
+
* both land — overshooting the ceiling by exactly the concurrency the HTTP face already has, and
|
|
101
|
+
* over-minting the seed-signed credentials the ceiling exists to bound. Occupancy is therefore
|
|
102
|
+
* live sessions PLUS in-flight reservations, and the increment happens with no `await` between
|
|
103
|
+
* the read and the write, so on a single-threaded runtime no second establishment can interleave.
|
|
104
|
+
*
|
|
105
|
+
* The returned release is idempotent: the caller releases on every failure path, and
|
|
106
|
+
* {@link establishAttach} releases at the same instant it records the live session, so a slot is
|
|
107
|
+
* counted once and never twice.
|
|
108
|
+
*
|
|
109
|
+
* The message names the ceiling and its current value: an operator who hits it should not have to
|
|
110
|
+
* read source to learn which knob to raise.
|
|
111
|
+
*/
|
|
112
|
+
claimSlot(): {
|
|
113
|
+
release(): void;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Open the DEDICATED sessions-bucket KV a plane's ledger needs (the `session.<id>` rows live in
|
|
117
|
+
* the §13.6 session store — {@link sessionsBucket}, NOT the auth bucket). Split out so the manager
|
|
118
|
+
* can build the plane from its own bucket handle. */
|
|
119
|
+
export declare function openSessionLedgerKv(nc: NatsConnection, bucket: string): Promise<KV>;
|
|
120
|
+
//# sourceMappingURL=plane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plane.d.ts","sourceRoot":"","sources":["../../src/session/plane.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,aAAa,EAElB,KAAK,YAAY,EACjB,KAAK,aAAa,EAGnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAiE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAExI,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAInD;;;;gGAIgG;AAChG,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,EAAE,GAAG,aAAa,CA4C5D;AAED;;;kFAGkF;AAClF,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,6GAA6G;IAC7G,OAAO,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;gEAC4D;IAC5D,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE;YAAE,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAAA;SAAE,CAAA;KAAE,CAAC;IAC5E,aAAa,EAAE,cAAc,CAAC;IAC9B,uGAAuG;IACvG,QAAQ,EAAE,EAAE,CAAC;IACb;qGACiG;IACjG,iBAAiB,EAAE,cAAc,CAAC;IAClC,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,IAAI,MAAM,CAAC;CAChB;AAUD,qBAAa,mBAAmB;;gBASlB,IAAI,EAAE,uBAAuB;IAQzC;;;2CAGuC;IACjC,eAAe,CACnB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EACrD,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,aAAa;IACtB;oGACgG;IAChG,OAAO,CAAC,EAAE;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,GAC5B,OAAO,CAAC;QAAE,KAAK,EAAE,YAAY,CAAA;KAAE,CAAC;IA0HnC;;;6FAGyF;IACzF,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAI/E,oDAAoD;IACpD,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;IAIrC;iGAC6F;IACvF,KAAK,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,2FAA2F;IAC3F,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,SAAS,IAAI;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE;CAUjC;AAED;;sDAEsD;AACtD,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,CAEzF"}
|