instar 1.3.608 → 1.3.609
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/commands/server.js +2 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/coordination/AccountFollowMeMandateBridge.d.ts +56 -0
- package/dist/coordination/AccountFollowMeMandateBridge.d.ts.map +1 -0
- package/dist/coordination/AccountFollowMeMandateBridge.js +56 -0
- package/dist/coordination/AccountFollowMeMandateBridge.js.map +1 -0
- package/dist/core/AccountFollowMeDetector.d.ts +53 -0
- package/dist/core/AccountFollowMeDetector.d.ts.map +1 -0
- package/dist/core/AccountFollowMeDetector.js +56 -0
- package/dist/core/AccountFollowMeDetector.js.map +1 -0
- package/dist/core/AccountFollowMeEmailGate.d.ts +51 -0
- package/dist/core/AccountFollowMeEmailGate.d.ts.map +1 -0
- package/dist/core/AccountFollowMeEmailGate.js +50 -0
- package/dist/core/AccountFollowMeEmailGate.js.map +1 -0
- package/dist/core/AccountFollowMeOrchestrator.d.ts +98 -0
- package/dist/core/AccountFollowMeOrchestrator.d.ts.map +1 -0
- package/dist/core/AccountFollowMeOrchestrator.js +76 -0
- package/dist/core/AccountFollowMeOrchestrator.js.map +1 -0
- package/dist/core/AccountFollowMeService.d.ts +84 -0
- package/dist/core/AccountFollowMeService.d.ts.map +1 -0
- package/dist/core/AccountFollowMeService.js +105 -0
- package/dist/core/AccountFollowMeService.js.map +1 -0
- package/dist/core/accountFollowMeDepth.d.ts +40 -0
- package/dist/core/accountFollowMeDepth.d.ts.map +1 -0
- package/dist/core/accountFollowMeDepth.js +47 -0
- package/dist/core/accountFollowMeDepth.js.map +1 -0
- package/dist/core/fetchPeerSubscriptionViews.d.ts +46 -0
- package/dist/core/fetchPeerSubscriptionViews.d.ts.map +1 -0
- package/dist/core/fetchPeerSubscriptionViews.js +70 -0
- package/dist/core/fetchPeerSubscriptionViews.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +1 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +49 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/1.3.609.md +24 -0
- package/upgrades/side-effects/ws52-account-follow-me-pr2.md +59 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.2 + R4a — cross-machine delivery of an `account-follow-me` mandate.
|
|
3
|
+
*
|
|
4
|
+
* Coordination mandates are issued (PIN-gated) and verified LOCALLY today; their authorship proof
|
|
5
|
+
* is a SYMMETRIC HMAC over the per-machine issuance secret, which a PEER cannot verify (§3.1a).
|
|
6
|
+
* For account-follow-me the mandate is issued on the OPERATOR machine but must be acted on by the
|
|
7
|
+
* TARGET machine — so it has to cross the mesh and be verified there WITHOUT a shared secret.
|
|
8
|
+
*
|
|
9
|
+
* This bridge wires PR1's asymmetric primitives (CrossMachineMandate.signMandateIssuance /
|
|
10
|
+
* verifyMandateIssuance, R4a) onto the mandate's canonical bytes:
|
|
11
|
+
* - on the operator machine: sign the canonical mandate with the machine's Ed25519 IDENTITY key,
|
|
12
|
+
* bound to its fingerprint, producing a portable issuance signature;
|
|
13
|
+
* - on the target machine: verify that signature against the REGISTERED operator-machine's
|
|
14
|
+
* Ed25519 public key + expected fingerprint (verified-operator binding) BEFORE acting. A
|
|
15
|
+
* forged/edited/wrong-issuer mandate is rejected (deny-by-default; reach ≠ authority, L15).
|
|
16
|
+
*
|
|
17
|
+
* The bridge does NOT itself grant anything — a verified mandate still flows through MandateGate
|
|
18
|
+
* (via AccountFollowMeOrchestrator) for the exact (account, target, mechanism) bounds check. This
|
|
19
|
+
* is the transport+authenticity layer only. Pure logic + injected crypto; PR2 increment 3b.
|
|
20
|
+
*/
|
|
21
|
+
import crypto from 'node:crypto';
|
|
22
|
+
import { type CrossMachineIssuanceSignature } from './CrossMachineMandate.js';
|
|
23
|
+
import type { CoordinationMandate } from './types.js';
|
|
24
|
+
/** A mandate packaged for cross-machine delivery: the mandate + its asymmetric issuance signature. */
|
|
25
|
+
export interface PortableMandate {
|
|
26
|
+
mandate: CoordinationMandate;
|
|
27
|
+
issuanceSignature: CrossMachineIssuanceSignature;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sign an issued mandate for cross-machine delivery (operator machine side). The canonical bytes
|
|
31
|
+
* are the SAME authored-field projection the local authProof covers, so the asymmetric signature
|
|
32
|
+
* and the local HMAC proof attest to identical content.
|
|
33
|
+
*/
|
|
34
|
+
export declare function packageMandateForDelivery(mandate: CoordinationMandate, issuerFingerprint: string, ed25519PrivateKey: crypto.KeyObject): PortableMandate;
|
|
35
|
+
export type MandateAcceptResult = {
|
|
36
|
+
accepted: true;
|
|
37
|
+
mandate: CoordinationMandate;
|
|
38
|
+
} | {
|
|
39
|
+
accepted: false;
|
|
40
|
+
reason: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Verify a delivered mandate on the TARGET machine (R4a). FAILS CLOSED: the asymmetric issuance
|
|
44
|
+
* signature must verify against the EXPECTED registered operator-machine's Ed25519 key + fingerprint
|
|
45
|
+
* (a name in the payload is never trusted — only the cryptographic + verified-operator binding).
|
|
46
|
+
* On success the mandate is returned for the orchestrator/gate to evaluate the exact bounds; this
|
|
47
|
+
* layer asserts ONLY "this mandate was genuinely issued by my trusted operator machine."
|
|
48
|
+
*/
|
|
49
|
+
export declare function acceptDeliveredMandate(args: {
|
|
50
|
+
portable: PortableMandate | undefined | null;
|
|
51
|
+
/** The trusted operator-machine's Ed25519 public key (registered MachineIdentity key). */
|
|
52
|
+
operatorEd25519PublicKey: crypto.KeyObject | string | Buffer;
|
|
53
|
+
/** The fingerprint the target TRUSTS as its operator machine (verified-operator binding). */
|
|
54
|
+
expectedOperatorMachineFingerprint: string;
|
|
55
|
+
}): MandateAcceptResult;
|
|
56
|
+
//# sourceMappingURL=AccountFollowMeMandateBridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeMandateBridge.d.ts","sourceRoot":"","sources":["../../src/coordination/AccountFollowMeMandateBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,OAAO,EAA8C,KAAK,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAC1H,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,sGAAsG;AACtG,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,mBAAmB,CAAC;IAC7B,iBAAiB,EAAE,6BAA6B,CAAC;CAClD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,mBAAmB,EAC5B,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,CAAC,SAAS,GAClC,eAAe,CAIjB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE,GAChD;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,QAAQ,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI,CAAC;IAC7C,0FAA0F;IAC1F,wBAAwB,EAAE,MAAM,CAAC,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC7D,6FAA6F;IAC7F,kCAAkC,EAAE,MAAM,CAAC;CAC5C,GAAG,mBAAmB,CActB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.2 + R4a — cross-machine delivery of an `account-follow-me` mandate.
|
|
3
|
+
*
|
|
4
|
+
* Coordination mandates are issued (PIN-gated) and verified LOCALLY today; their authorship proof
|
|
5
|
+
* is a SYMMETRIC HMAC over the per-machine issuance secret, which a PEER cannot verify (§3.1a).
|
|
6
|
+
* For account-follow-me the mandate is issued on the OPERATOR machine but must be acted on by the
|
|
7
|
+
* TARGET machine — so it has to cross the mesh and be verified there WITHOUT a shared secret.
|
|
8
|
+
*
|
|
9
|
+
* This bridge wires PR1's asymmetric primitives (CrossMachineMandate.signMandateIssuance /
|
|
10
|
+
* verifyMandateIssuance, R4a) onto the mandate's canonical bytes:
|
|
11
|
+
* - on the operator machine: sign the canonical mandate with the machine's Ed25519 IDENTITY key,
|
|
12
|
+
* bound to its fingerprint, producing a portable issuance signature;
|
|
13
|
+
* - on the target machine: verify that signature against the REGISTERED operator-machine's
|
|
14
|
+
* Ed25519 public key + expected fingerprint (verified-operator binding) BEFORE acting. A
|
|
15
|
+
* forged/edited/wrong-issuer mandate is rejected (deny-by-default; reach ≠ authority, L15).
|
|
16
|
+
*
|
|
17
|
+
* The bridge does NOT itself grant anything — a verified mandate still flows through MandateGate
|
|
18
|
+
* (via AccountFollowMeOrchestrator) for the exact (account, target, mechanism) bounds check. This
|
|
19
|
+
* is the transport+authenticity layer only. Pure logic + injected crypto; PR2 increment 3b.
|
|
20
|
+
*/
|
|
21
|
+
import { canonicalMandate } from './MandateStore.js';
|
|
22
|
+
import { signMandateIssuance, verifyMandateIssuance } from './CrossMachineMandate.js';
|
|
23
|
+
/**
|
|
24
|
+
* Sign an issued mandate for cross-machine delivery (operator machine side). The canonical bytes
|
|
25
|
+
* are the SAME authored-field projection the local authProof covers, so the asymmetric signature
|
|
26
|
+
* and the local HMAC proof attest to identical content.
|
|
27
|
+
*/
|
|
28
|
+
export function packageMandateForDelivery(mandate, issuerFingerprint, ed25519PrivateKey) {
|
|
29
|
+
const canonical = canonicalMandate(mandate);
|
|
30
|
+
const issuanceSignature = signMandateIssuance(canonical, issuerFingerprint, ed25519PrivateKey);
|
|
31
|
+
return { mandate, issuanceSignature };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Verify a delivered mandate on the TARGET machine (R4a). FAILS CLOSED: the asymmetric issuance
|
|
35
|
+
* signature must verify against the EXPECTED registered operator-machine's Ed25519 key + fingerprint
|
|
36
|
+
* (a name in the payload is never trusted — only the cryptographic + verified-operator binding).
|
|
37
|
+
* On success the mandate is returned for the orchestrator/gate to evaluate the exact bounds; this
|
|
38
|
+
* layer asserts ONLY "this mandate was genuinely issued by my trusted operator machine."
|
|
39
|
+
*/
|
|
40
|
+
export function acceptDeliveredMandate(args) {
|
|
41
|
+
const { portable, operatorEd25519PublicKey, expectedOperatorMachineFingerprint } = args;
|
|
42
|
+
if (!portable || !portable.mandate || !portable.issuanceSignature) {
|
|
43
|
+
return { accepted: false, reason: 'malformed-portable-mandate' };
|
|
44
|
+
}
|
|
45
|
+
const canonical = canonicalMandate(portable.mandate);
|
|
46
|
+
const v = verifyMandateIssuance({
|
|
47
|
+
canonical,
|
|
48
|
+
signature: portable.issuanceSignature,
|
|
49
|
+
issuerEd25519PublicKey: operatorEd25519PublicKey,
|
|
50
|
+
expectedIssuerFingerprint: expectedOperatorMachineFingerprint,
|
|
51
|
+
});
|
|
52
|
+
if (!v.ok)
|
|
53
|
+
return { accepted: false, reason: `issuance-verify-failed:${v.reason ?? 'unknown'}` };
|
|
54
|
+
return { accepted: true, mandate: portable.mandate };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=AccountFollowMeMandateBridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeMandateBridge.js","sourceRoot":"","sources":["../../src/coordination/AccountFollowMeMandateBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAsC,MAAM,0BAA0B,CAAC;AAS1H;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAA4B,EAC5B,iBAAyB,EACzB,iBAAmC;IAEnC,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,iBAAiB,GAAG,mBAAmB,CAAC,SAAS,EAAE,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;IAC/F,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC;AACxC,CAAC;AAMD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAMtC;IACC,MAAM,EAAE,QAAQ,EAAE,wBAAwB,EAAE,kCAAkC,EAAE,GAAG,IAAI,CAAC;IACxF,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,iBAAiB,EAAE,CAAC;QAClE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IACnE,CAAC;IACD,MAAM,SAAS,GAAG,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACrD,MAAM,CAAC,GAAG,qBAAqB,CAAC;QAC9B,SAAS;QACT,SAAS,EAAE,QAAQ,CAAC,iBAAiB;QACrC,sBAAsB,EAAE,wBAAwB;QAChD,yBAAyB,EAAE,kCAAkC;KAC9D,CAAC,CAAC;IACH,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,0BAA0B,CAAC,CAAC,MAAM,IAAI,SAAS,EAAE,EAAE,CAAC;IACjG,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.2 + R7 — depth-zero enrollment-offer detector.
|
|
3
|
+
*
|
|
4
|
+
* Decides WHICH (account → target-machine) enrollment offers to surface, from the pool's
|
|
5
|
+
* per-machine account state. A "depth-zero" machine has NO usable account for an operator
|
|
6
|
+
* subscription; the agent should OFFER (never auto-do) to enroll one there. This is the pure
|
|
7
|
+
* decision layer that feeds AccountFollowMeOrchestrator (§5.2) — it decides the candidate offers;
|
|
8
|
+
* the orchestrator still requires an operator mandate before anything happens.
|
|
9
|
+
*
|
|
10
|
+
* R7 bounds (so adding the Mth machine can't multiply traffic):
|
|
11
|
+
* - only machines with ZERO usable accounts are candidates;
|
|
12
|
+
* - per-account **max-follow-machines cap** — never offer an account to more targets than the cap
|
|
13
|
+
* (counting machines that ALREADY hold it);
|
|
14
|
+
* - **one offer per (account, target)** — an offer already in-flight (a pending mandate/consent)
|
|
15
|
+
* is not re-emitted;
|
|
16
|
+
* - the caller raises ONE AGGREGATED consent item over the returned list (P17), never one per offer.
|
|
17
|
+
*
|
|
18
|
+
* Pure + deterministic (no I/O); unit-tested. PR2 increment 3a.
|
|
19
|
+
*/
|
|
20
|
+
export interface PoolMachineDepth {
|
|
21
|
+
machineId: string;
|
|
22
|
+
nickname: string;
|
|
23
|
+
/** Count of accounts this machine can actually serve from (a real local login). 0 = depth-zero. */
|
|
24
|
+
usableAccountCount: number;
|
|
25
|
+
}
|
|
26
|
+
export interface OperatorAccount {
|
|
27
|
+
accountId: string;
|
|
28
|
+
email: string;
|
|
29
|
+
/** Machine ids that ALREADY hold a usable login for this account (re-mint instances). */
|
|
30
|
+
heldByMachineIds: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface EnrollmentOffer {
|
|
33
|
+
accountId: string;
|
|
34
|
+
accountEmail: string;
|
|
35
|
+
targetMachineId: string;
|
|
36
|
+
targetMachineNickname: string;
|
|
37
|
+
}
|
|
38
|
+
export interface DetectInput {
|
|
39
|
+
machines: PoolMachineDepth[];
|
|
40
|
+
accounts: OperatorAccount[];
|
|
41
|
+
/** Per-account cap on total machines that may hold it (R7; small default e.g. 5). */
|
|
42
|
+
maxFollowMachines: number;
|
|
43
|
+
/** (account, target) pairs with an offer/mandate already in flight — never re-offered. */
|
|
44
|
+
inFlight?: ReadonlySet<string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compute the enrollment offers to surface. Deterministic: candidates are depth-zero machines,
|
|
48
|
+
* each offered the operator's "primary" account (the most-held one, stable tie-break by accountId),
|
|
49
|
+
* subject to the per-account cap and the in-flight dedup. Returns at most ONE offer per target
|
|
50
|
+
* machine (a machine needs only one account to stop being depth-zero).
|
|
51
|
+
*/
|
|
52
|
+
export declare function detectEnrollmentOffers(input: DetectInput): EnrollmentOffer[];
|
|
53
|
+
//# sourceMappingURL=AccountFollowMeDetector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeDetector.d.ts","sourceRoot":"","sources":["../../src/core/AccountFollowMeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,yFAAyF;IACzF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,qFAAqF;IACrF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC;AAID;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,EAAE,CA4B5E"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.2 + R7 — depth-zero enrollment-offer detector.
|
|
3
|
+
*
|
|
4
|
+
* Decides WHICH (account → target-machine) enrollment offers to surface, from the pool's
|
|
5
|
+
* per-machine account state. A "depth-zero" machine has NO usable account for an operator
|
|
6
|
+
* subscription; the agent should OFFER (never auto-do) to enroll one there. This is the pure
|
|
7
|
+
* decision layer that feeds AccountFollowMeOrchestrator (§5.2) — it decides the candidate offers;
|
|
8
|
+
* the orchestrator still requires an operator mandate before anything happens.
|
|
9
|
+
*
|
|
10
|
+
* R7 bounds (so adding the Mth machine can't multiply traffic):
|
|
11
|
+
* - only machines with ZERO usable accounts are candidates;
|
|
12
|
+
* - per-account **max-follow-machines cap** — never offer an account to more targets than the cap
|
|
13
|
+
* (counting machines that ALREADY hold it);
|
|
14
|
+
* - **one offer per (account, target)** — an offer already in-flight (a pending mandate/consent)
|
|
15
|
+
* is not re-emitted;
|
|
16
|
+
* - the caller raises ONE AGGREGATED consent item over the returned list (P17), never one per offer.
|
|
17
|
+
*
|
|
18
|
+
* Pure + deterministic (no I/O); unit-tested. PR2 increment 3a.
|
|
19
|
+
*/
|
|
20
|
+
const key = (accountId, targetMachineId) => `${accountId}::${targetMachineId}`;
|
|
21
|
+
/**
|
|
22
|
+
* Compute the enrollment offers to surface. Deterministic: candidates are depth-zero machines,
|
|
23
|
+
* each offered the operator's "primary" account (the most-held one, stable tie-break by accountId),
|
|
24
|
+
* subject to the per-account cap and the in-flight dedup. Returns at most ONE offer per target
|
|
25
|
+
* machine (a machine needs only one account to stop being depth-zero).
|
|
26
|
+
*/
|
|
27
|
+
export function detectEnrollmentOffers(input) {
|
|
28
|
+
const inFlight = input.inFlight ?? new Set();
|
|
29
|
+
const cap = Number.isFinite(input.maxFollowMachines) && input.maxFollowMachines > 0 ? input.maxFollowMachines : 5;
|
|
30
|
+
// Mutable copy of how many machines each account is committed to (held + offers we emit this pass).
|
|
31
|
+
const committed = new Map();
|
|
32
|
+
for (const a of input.accounts)
|
|
33
|
+
committed.set(a.accountId, new Set(a.heldByMachineIds).size);
|
|
34
|
+
// Prefer the account already on the most machines (the operator's "main"), stable by accountId.
|
|
35
|
+
const ranked = [...input.accounts].sort((x, y) => y.heldByMachineIds.length - x.heldByMachineIds.length || x.accountId.localeCompare(y.accountId));
|
|
36
|
+
const offers = [];
|
|
37
|
+
for (const m of input.machines) {
|
|
38
|
+
if (m.usableAccountCount > 0)
|
|
39
|
+
continue; // not depth-zero
|
|
40
|
+
// Pick the first ranked account that (a) this machine doesn't already hold, (b) is under cap,
|
|
41
|
+
// (c) has no in-flight offer for this (account, target).
|
|
42
|
+
for (const a of ranked) {
|
|
43
|
+
if (a.heldByMachineIds.includes(m.machineId))
|
|
44
|
+
continue;
|
|
45
|
+
if ((committed.get(a.accountId) ?? 0) >= cap)
|
|
46
|
+
continue;
|
|
47
|
+
if (inFlight.has(key(a.accountId, m.machineId)))
|
|
48
|
+
continue;
|
|
49
|
+
offers.push({ accountId: a.accountId, accountEmail: a.email, targetMachineId: m.machineId, targetMachineNickname: m.nickname });
|
|
50
|
+
committed.set(a.accountId, (committed.get(a.accountId) ?? 0) + 1);
|
|
51
|
+
break; // one offer per target machine
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return offers;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=AccountFollowMeDetector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeDetector.js","sourceRoot":"","sources":["../../src/core/AccountFollowMeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgCH,MAAM,GAAG,GAAG,CAAC,SAAiB,EAAE,eAAuB,EAAE,EAAE,CAAC,GAAG,SAAS,KAAK,eAAe,EAAE,CAAC;AAE/F;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAkB;IACvD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,IAAI,GAAG,EAAU,CAAC;IACrD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;IAElH,oGAAoG;IACpG,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ;QAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC;IAE7F,gGAAgG;IAChG,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CACrC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,CAAC,gBAAgB,CAAC,MAAM,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAC1G,CAAC;IAEF,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,CAAC,kBAAkB,GAAG,CAAC;YAAE,SAAS,CAAC,iBAAiB;QACzD,8FAA8F;QAC9F,yDAAyD;QACzD,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,IAAI,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;gBAAE,SAAS;YACvD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG;gBAAE,SAAS;YACvD,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;gBAAE,SAAS;YAC1D,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,SAAS,EAAE,qBAAqB,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;YAChI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAClE,MAAM,CAAC,+BAA+B;QACxC,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.3 step 3 / S7 — email-validation-before-selectable.
|
|
3
|
+
*
|
|
4
|
+
* When an operator-authorized follow-me enrollment COMPLETES on a target machine, the freshly
|
|
5
|
+
* minted login's account email MUST be validated against the operator's expectation BEFORE the
|
|
6
|
+
* account becomes selectable. A surprise email (the operator approved account A on their phone,
|
|
7
|
+
* but the login that completed is a DIFFERENT account B) must NOT be auto-used — it raises an
|
|
8
|
+
* attention item for the operator instead. This closes S7: "the target is trusted with a live
|
|
9
|
+
* credential; host+email validated" — a wrong/unexpected account never silently becomes the one
|
|
10
|
+
* the agent serves from.
|
|
11
|
+
*
|
|
12
|
+
* Standalone gate (NOT a mutation of the shared EnrollmentWizard.complete): the follow-me path
|
|
13
|
+
* calls this with the completed email + the operator-expected email; only on a verified match
|
|
14
|
+
* does it call SubscriptionPool.add() and mark the account selectable.
|
|
15
|
+
*
|
|
16
|
+
* PR2 increment 2: the gate + its tests. Wired into the §5.3 completion path in a later increment.
|
|
17
|
+
*/
|
|
18
|
+
export interface EmailValidationInput {
|
|
19
|
+
/** The account email the COMPLETED login actually authenticated as (from the provider). */
|
|
20
|
+
completedEmail: string | null | undefined;
|
|
21
|
+
/** The account email the operator EXPECTED (the mandate's account; what they approved). */
|
|
22
|
+
expectedEmail: string | null | undefined;
|
|
23
|
+
/** The account id being enrolled (for the attention item). */
|
|
24
|
+
accountId: string;
|
|
25
|
+
/** Operator-facing nickname of the target machine (for the attention item). */
|
|
26
|
+
targetMachineNickname: string;
|
|
27
|
+
}
|
|
28
|
+
export interface EmailMismatchAttentionItem {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
body: string;
|
|
32
|
+
priority: 'high';
|
|
33
|
+
source: 'agent';
|
|
34
|
+
}
|
|
35
|
+
export type EmailValidationResult = {
|
|
36
|
+
selectable: true;
|
|
37
|
+
email: string;
|
|
38
|
+
} | {
|
|
39
|
+
selectable: false;
|
|
40
|
+
reason: 'email-mismatch' | 'missing-completed-email' | 'missing-expected-email';
|
|
41
|
+
expected: string | null;
|
|
42
|
+
got: string | null;
|
|
43
|
+
attentionItem: EmailMismatchAttentionItem;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Validate a completed follow-me enrollment's email against operator expectation (S7).
|
|
47
|
+
* FAILS CLOSED (selectable:false) on any mismatch OR on a missing completed/expected email —
|
|
48
|
+
* an account is NEVER auto-selected unless its email provably matches what the operator approved.
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateEnrolledAccountEmail(input: EmailValidationInput): EmailValidationResult;
|
|
51
|
+
//# sourceMappingURL=AccountFollowMeEmailGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeEmailGate.d.ts","sourceRoot":"","sources":["../../src/core/AccountFollowMeEmailGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,oBAAoB;IACnC,2FAA2F;IAC3F,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1C,2FAA2F;IAC3F,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IACE,UAAU,EAAE,KAAK,CAAC;IAClB,MAAM,EAAE,gBAAgB,GAAG,yBAAyB,GAAG,wBAAwB,CAAC;IAChF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,aAAa,EAAE,0BAA0B,CAAC;CAC3C,CAAC;AAON;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,oBAAoB,GAAG,qBAAqB,CAyB/F"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.3 step 3 / S7 — email-validation-before-selectable.
|
|
3
|
+
*
|
|
4
|
+
* When an operator-authorized follow-me enrollment COMPLETES on a target machine, the freshly
|
|
5
|
+
* minted login's account email MUST be validated against the operator's expectation BEFORE the
|
|
6
|
+
* account becomes selectable. A surprise email (the operator approved account A on their phone,
|
|
7
|
+
* but the login that completed is a DIFFERENT account B) must NOT be auto-used — it raises an
|
|
8
|
+
* attention item for the operator instead. This closes S7: "the target is trusted with a live
|
|
9
|
+
* credential; host+email validated" — a wrong/unexpected account never silently becomes the one
|
|
10
|
+
* the agent serves from.
|
|
11
|
+
*
|
|
12
|
+
* Standalone gate (NOT a mutation of the shared EnrollmentWizard.complete): the follow-me path
|
|
13
|
+
* calls this with the completed email + the operator-expected email; only on a verified match
|
|
14
|
+
* does it call SubscriptionPool.add() and mark the account selectable.
|
|
15
|
+
*
|
|
16
|
+
* PR2 increment 2: the gate + its tests. Wired into the §5.3 completion path in a later increment.
|
|
17
|
+
*/
|
|
18
|
+
/** Normalize an email for comparison (trim + lowercase). Returns '' for nullish/non-string. */
|
|
19
|
+
function normEmail(v) {
|
|
20
|
+
return typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate a completed follow-me enrollment's email against operator expectation (S7).
|
|
24
|
+
* FAILS CLOSED (selectable:false) on any mismatch OR on a missing completed/expected email —
|
|
25
|
+
* an account is NEVER auto-selected unless its email provably matches what the operator approved.
|
|
26
|
+
*/
|
|
27
|
+
export function validateEnrolledAccountEmail(input) {
|
|
28
|
+
const got = normEmail(input.completedEmail);
|
|
29
|
+
const expected = normEmail(input.expectedEmail);
|
|
30
|
+
const attention = (reason) => ({
|
|
31
|
+
id: `agent:account-follow-me-email:${input.accountId}:${input.targetMachineNickname.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`,
|
|
32
|
+
title: `Account enrollment needs your check on "${input.targetMachineNickname}"`,
|
|
33
|
+
body: `An account-follow-me enrollment on "${input.targetMachineNickname}" ${reason}. ` +
|
|
34
|
+
`Expected ${input.expectedEmail || '(unspecified)'}, got ${input.completedEmail || '(none)'}. ` +
|
|
35
|
+
`The account was NOT auto-selected — review before it is used.`,
|
|
36
|
+
priority: 'high',
|
|
37
|
+
source: 'agent',
|
|
38
|
+
});
|
|
39
|
+
if (!got) {
|
|
40
|
+
return { selectable: false, reason: 'missing-completed-email', expected: expected || null, got: null, attentionItem: attention('completed without a verifiable account email') };
|
|
41
|
+
}
|
|
42
|
+
if (!expected) {
|
|
43
|
+
return { selectable: false, reason: 'missing-expected-email', expected: null, got: input.completedEmail ?? null, attentionItem: attention('has no operator-expected email to validate against') };
|
|
44
|
+
}
|
|
45
|
+
if (got !== expected) {
|
|
46
|
+
return { selectable: false, reason: 'email-mismatch', expected: input.expectedEmail ?? null, got: input.completedEmail ?? null, attentionItem: attention('authenticated as a DIFFERENT account than approved') };
|
|
47
|
+
}
|
|
48
|
+
return { selectable: true, email: input.completedEmail.trim() };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=AccountFollowMeEmailGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeEmailGate.js","sourceRoot":"","sources":["../../src/core/AccountFollowMeEmailGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AA+BH,+FAA+F;AAC/F,SAAS,SAAS,CAAC,CAAU;IAC3B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAA2B;IACtE,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IAEhD,MAAM,SAAS,GAAG,CAAC,MAAc,EAA8B,EAAE,CAAC,CAAC;QACjE,EAAE,EAAE,iCAAiC,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,qBAAqB,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE;QAChI,KAAK,EAAE,2CAA2C,KAAK,CAAC,qBAAqB,GAAG;QAChF,IAAI,EACF,uCAAuC,KAAK,CAAC,qBAAqB,KAAK,MAAM,IAAI;YACjF,YAAY,KAAK,CAAC,aAAa,IAAI,eAAe,SAAS,KAAK,CAAC,cAAc,IAAI,QAAQ,IAAI;YAC/F,+DAA+D;QACjE,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,OAAO;KAChB,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,yBAAyB,EAAE,QAAQ,EAAE,QAAQ,IAAI,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,CAAC,8CAA8C,CAAC,EAAE,CAAC;IACnL,CAAC;IACD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,cAAc,IAAI,IAAI,EAAE,aAAa,EAAE,SAAS,CAAC,oDAAoD,CAAC,EAAE,CAAC;IACpM,CAAC;IACD,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;QACrB,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,CAAC,aAAa,IAAI,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,cAAc,IAAI,IAAI,EAAE,aAAa,EAAE,SAAS,CAAC,oDAAoD,CAAC,EAAE,CAAC;IACnN,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,cAAe,CAAC,IAAI,EAAE,EAAE,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.2 — Account Follow-Me: the agent's role is REQUEST, never self-authorize.
|
|
3
|
+
*
|
|
4
|
+
* When the agent detects a machine with no usable account for an operator's subscription
|
|
5
|
+
* (depth-zero; WS5.1 already detects this), it must NOT enroll that machine on its own. The
|
|
6
|
+
* ONLY authorizer is a PIN-gated operator mandate (`action: 'account-follow-me'`). This
|
|
7
|
+
* orchestrator runs the deny-by-default flow:
|
|
8
|
+
*
|
|
9
|
+
* - evaluate the mandate gate for (accountId, targetMachineId, mechanism);
|
|
10
|
+
* - DENY (no mandate yet, or bounds/expiry/revocation/forgery) → surface a PHONE-FIRST
|
|
11
|
+
* consent request (a deep-link to the pre-filled Mandates form) and DO NOT proceed;
|
|
12
|
+
* - ALLOW → return proceed:true so the caller drives Mechanism B (§5.3).
|
|
13
|
+
*
|
|
14
|
+
* Load-bearing safety (reach ≠ authority, L15): a peer mesh message claiming "the operator said
|
|
15
|
+
* enroll me" carries NO authority — with no operator-issued mandate the gate denies, and this
|
|
16
|
+
* orchestrator surfaces consent instead of proceeding. The consent surface is NEVER a CLI
|
|
17
|
+
* instruction ("run instar …") — it is a dashboard deep-link the operator taps (parent spec
|
|
18
|
+
* B2). Mechanism defaults to 're-mint' (ToS-safe); 'credential-transport' is only ever reached
|
|
19
|
+
* when an explicit mandate authorizes it AND the provider is allowlisted (refused for Anthropic).
|
|
20
|
+
*
|
|
21
|
+
* PR2 increment 1: this orchestrator + its tests. Later increments wire the depth-zero detector
|
|
22
|
+
* to it and drive the EnrollmentWizard on allow (§5.3, per-server transport per OQ6).
|
|
23
|
+
*/
|
|
24
|
+
export type FollowMeMechanism = 're-mint' | 'credential-transport';
|
|
25
|
+
export interface FollowMeRequest {
|
|
26
|
+
/** SubscriptionAccount.id the operator would extend to the target machine. */
|
|
27
|
+
accountId: string;
|
|
28
|
+
/** Account email (operator-facing; shown in the consent prompt). */
|
|
29
|
+
accountEmail: string;
|
|
30
|
+
/** The machine that has no usable account (the enrollment target). */
|
|
31
|
+
targetMachineId: string;
|
|
32
|
+
/** Operator-facing nickname of the target machine (shown in the consent prompt). */
|
|
33
|
+
targetMachineNickname: string;
|
|
34
|
+
/** Default 're-mint' (ToS-safe). 'credential-transport' needs an explicit allowlisted mandate. */
|
|
35
|
+
mechanism?: FollowMeMechanism;
|
|
36
|
+
/** The operator mandate id, if one has been issued for this (account, target). Absent ⇒ deny. */
|
|
37
|
+
mandateId?: string;
|
|
38
|
+
}
|
|
39
|
+
/** A phone-first consent request — a dashboard deep-link, NEVER a CLI instruction. */
|
|
40
|
+
export interface FollowMeConsentRequest {
|
|
41
|
+
kind: 'account-follow-me-consent';
|
|
42
|
+
accountId: string;
|
|
43
|
+
accountEmail: string;
|
|
44
|
+
targetMachineId: string;
|
|
45
|
+
targetMachineNickname: string;
|
|
46
|
+
mechanism: FollowMeMechanism;
|
|
47
|
+
/** Pre-filled Mandates-form deep link (account + target pre-selected). */
|
|
48
|
+
dashboardDeepLink: string;
|
|
49
|
+
/** The plain-English message to surface to the operator. */
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
export type FollowMeDecision = {
|
|
53
|
+
proceed: true;
|
|
54
|
+
accountId: string;
|
|
55
|
+
targetMachineId: string;
|
|
56
|
+
mechanism: FollowMeMechanism;
|
|
57
|
+
reason: string;
|
|
58
|
+
} | {
|
|
59
|
+
proceed: false;
|
|
60
|
+
outcome: 'consent-required';
|
|
61
|
+
consent: FollowMeConsentRequest;
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
export interface MandateGateLike {
|
|
65
|
+
evaluate(ev: {
|
|
66
|
+
action: string;
|
|
67
|
+
params: Record<string, unknown>;
|
|
68
|
+
agentFp: string;
|
|
69
|
+
mandateId: string;
|
|
70
|
+
}): {
|
|
71
|
+
decision: 'allow' | 'deny';
|
|
72
|
+
reason: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface AccountFollowMeOrchestratorDeps {
|
|
76
|
+
/** The operator-mandate gate (MandateGate.evaluate). */
|
|
77
|
+
gate: MandateGateLike;
|
|
78
|
+
/** This agent's routing fingerprint (the named-party check in the gate). */
|
|
79
|
+
agentFp: () => string;
|
|
80
|
+
/** Build the pre-filled Mandates-form deep link (account + target pre-selected). */
|
|
81
|
+
mandatesDeepLink: (args: {
|
|
82
|
+
accountId: string;
|
|
83
|
+
targetMachineId: string;
|
|
84
|
+
mechanism: FollowMeMechanism;
|
|
85
|
+
}) => string;
|
|
86
|
+
log?: (msg: string) => void;
|
|
87
|
+
}
|
|
88
|
+
export declare class AccountFollowMeOrchestrator {
|
|
89
|
+
private readonly deps;
|
|
90
|
+
constructor(deps: AccountFollowMeOrchestratorDeps);
|
|
91
|
+
/**
|
|
92
|
+
* §5.2 request-never-self-authorize. Returns proceed:true ONLY when a real operator mandate
|
|
93
|
+
* authorizes this exact (account, target, mechanism); otherwise returns a phone-first consent
|
|
94
|
+
* request and DOES NOT proceed. Never self-authorizes.
|
|
95
|
+
*/
|
|
96
|
+
requestEnrollment(req: FollowMeRequest): FollowMeDecision;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=AccountFollowMeOrchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeOrchestrator.d.ts","sourceRoot":"","sources":["../../src/core/AccountFollowMeOrchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,sBAAsB,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,qBAAqB,EAAE,MAAM,CAAC;IAC9B,kGAAkG;IAClG,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,iGAAiG;IACjG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,sFAAsF;AACtF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,2BAA2B,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3G;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,sBAAsB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAErG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE;QACX,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG;QAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACpD;AAED,MAAM,WAAW,+BAA+B;IAC9C,wDAAwD;IACxD,IAAI,EAAE,eAAe,CAAC;IACtB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,oFAAoF;IACpF,gBAAgB,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,iBAAiB,CAAA;KAAE,KAAK,MAAM,CAAC;IACjH,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,qBAAa,2BAA2B;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,+BAA+B;IAElE;;;;OAIG;IACH,iBAAiB,CAAC,GAAG,EAAE,eAAe,GAAG,gBAAgB;CA+C1D"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.2 — Account Follow-Me: the agent's role is REQUEST, never self-authorize.
|
|
3
|
+
*
|
|
4
|
+
* When the agent detects a machine with no usable account for an operator's subscription
|
|
5
|
+
* (depth-zero; WS5.1 already detects this), it must NOT enroll that machine on its own. The
|
|
6
|
+
* ONLY authorizer is a PIN-gated operator mandate (`action: 'account-follow-me'`). This
|
|
7
|
+
* orchestrator runs the deny-by-default flow:
|
|
8
|
+
*
|
|
9
|
+
* - evaluate the mandate gate for (accountId, targetMachineId, mechanism);
|
|
10
|
+
* - DENY (no mandate yet, or bounds/expiry/revocation/forgery) → surface a PHONE-FIRST
|
|
11
|
+
* consent request (a deep-link to the pre-filled Mandates form) and DO NOT proceed;
|
|
12
|
+
* - ALLOW → return proceed:true so the caller drives Mechanism B (§5.3).
|
|
13
|
+
*
|
|
14
|
+
* Load-bearing safety (reach ≠ authority, L15): a peer mesh message claiming "the operator said
|
|
15
|
+
* enroll me" carries NO authority — with no operator-issued mandate the gate denies, and this
|
|
16
|
+
* orchestrator surfaces consent instead of proceeding. The consent surface is NEVER a CLI
|
|
17
|
+
* instruction ("run instar …") — it is a dashboard deep-link the operator taps (parent spec
|
|
18
|
+
* B2). Mechanism defaults to 're-mint' (ToS-safe); 'credential-transport' is only ever reached
|
|
19
|
+
* when an explicit mandate authorizes it AND the provider is allowlisted (refused for Anthropic).
|
|
20
|
+
*
|
|
21
|
+
* PR2 increment 1: this orchestrator + its tests. Later increments wire the depth-zero detector
|
|
22
|
+
* to it and drive the EnrollmentWizard on allow (§5.3, per-server transport per OQ6).
|
|
23
|
+
*/
|
|
24
|
+
export class AccountFollowMeOrchestrator {
|
|
25
|
+
deps;
|
|
26
|
+
constructor(deps) {
|
|
27
|
+
this.deps = deps;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* §5.2 request-never-self-authorize. Returns proceed:true ONLY when a real operator mandate
|
|
31
|
+
* authorizes this exact (account, target, mechanism); otherwise returns a phone-first consent
|
|
32
|
+
* request and DOES NOT proceed. Never self-authorizes.
|
|
33
|
+
*/
|
|
34
|
+
requestEnrollment(req) {
|
|
35
|
+
const mechanism = req.mechanism ?? 're-mint';
|
|
36
|
+
const consent = () => ({
|
|
37
|
+
kind: 'account-follow-me-consent',
|
|
38
|
+
accountId: req.accountId,
|
|
39
|
+
accountEmail: req.accountEmail,
|
|
40
|
+
targetMachineId: req.targetMachineId,
|
|
41
|
+
targetMachineNickname: req.targetMachineNickname,
|
|
42
|
+
mechanism,
|
|
43
|
+
dashboardDeepLink: this.deps.mandatesDeepLink({
|
|
44
|
+
accountId: req.accountId,
|
|
45
|
+
targetMachineId: req.targetMachineId,
|
|
46
|
+
mechanism,
|
|
47
|
+
}),
|
|
48
|
+
message: `Machine "${req.targetMachineNickname}" has no account for ${req.accountEmail} yet. ` +
|
|
49
|
+
`Want it to use that subscription? Authorize on the dashboard.`,
|
|
50
|
+
});
|
|
51
|
+
// No mandate id at all ⇒ deny-by-default: never self-authorize, surface consent.
|
|
52
|
+
if (!req.mandateId) {
|
|
53
|
+
this.deps.log?.(`[account-follow-me] no mandate for ${req.accountId}→${req.targetMachineId}; surfacing consent`);
|
|
54
|
+
return { proceed: false, outcome: 'consent-required', consent: consent(), reason: 'no-mandate' };
|
|
55
|
+
}
|
|
56
|
+
const result = this.deps.gate.evaluate({
|
|
57
|
+
action: 'account-follow-me',
|
|
58
|
+
params: { accountId: req.accountId, targetMachineId: req.targetMachineId, mechanism },
|
|
59
|
+
agentFp: this.deps.agentFp(),
|
|
60
|
+
mandateId: req.mandateId,
|
|
61
|
+
});
|
|
62
|
+
if (result.decision !== 'allow') {
|
|
63
|
+
this.deps.log?.(`[account-follow-me] gate denied (${result.reason}); surfacing consent`);
|
|
64
|
+
return { proceed: false, outcome: 'consent-required', consent: consent(), reason: `denied:${result.reason}` };
|
|
65
|
+
}
|
|
66
|
+
this.deps.log?.(`[account-follow-me] gate allowed ${req.accountId}→${req.targetMachineId} (${mechanism})`);
|
|
67
|
+
return {
|
|
68
|
+
proceed: true,
|
|
69
|
+
accountId: req.accountId,
|
|
70
|
+
targetMachineId: req.targetMachineId,
|
|
71
|
+
mechanism,
|
|
72
|
+
reason: result.reason,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=AccountFollowMeOrchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeOrchestrator.js","sourceRoot":"","sources":["../../src/core/AccountFollowMeOrchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAwDH,MAAM,OAAO,2BAA2B;IACT;IAA7B,YAA6B,IAAqC;QAArC,SAAI,GAAJ,IAAI,CAAiC;IAAG,CAAC;IAEtE;;;;OAIG;IACH,iBAAiB,CAAC,GAAoB;QACpC,MAAM,SAAS,GAAsB,GAAG,CAAC,SAAS,IAAI,SAAS,CAAC;QAEhE,MAAM,OAAO,GAAG,GAA2B,EAAE,CAAC,CAAC;YAC7C,IAAI,EAAE,2BAA2B;YACjC,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,YAAY,EAAE,GAAG,CAAC,YAAY;YAC9B,eAAe,EAAE,GAAG,CAAC,eAAe;YACpC,qBAAqB,EAAE,GAAG,CAAC,qBAAqB;YAChD,SAAS;YACT,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC;gBAC5C,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,eAAe,EAAE,GAAG,CAAC,eAAe;gBACpC,SAAS;aACV,CAAC;YACF,OAAO,EACL,YAAY,GAAG,CAAC,qBAAqB,wBAAwB,GAAG,CAAC,YAAY,QAAQ;gBACrF,+DAA+D;SAClE,CAAC,CAAC;QAEH,iFAAiF;QACjF,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,sCAAsC,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,eAAe,qBAAqB,CAAC,CAAC;YACjH,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QACnG,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;YACrC,MAAM,EAAE,mBAAmB;YAC3B,MAAM,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,CAAC,eAAe,EAAE,SAAS,EAAE;YACrF,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;SACzB,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,oCAAoC,MAAM,CAAC,MAAM,sBAAsB,CAAC,CAAC;YACzF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,UAAU,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QAChH,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,oCAAoC,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,eAAe,KAAK,SAAS,GAAG,CAAC,CAAC;QAC3G,OAAO;YACL,OAAO,EAAE,IAAI;YACb,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,eAAe,EAAE,GAAG,CAAC,eAAe;YACpC,SAAS;YACT,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.2 — Account Follow-Me service: composes the primitives into the depth-zero → consent
|
|
3
|
+
* flow, and the mandate-delivered → proceed flow. Pure orchestration over INJECTED deps (pool
|
|
4
|
+
* reader, consent emitter, enroll driver) so it is unit-testable WITHOUT spawning a server; the
|
|
5
|
+
* HTTP route / scheduler tick later just constructs it with real deps.
|
|
6
|
+
*
|
|
7
|
+
* Two entry points:
|
|
8
|
+
* - scanAndOffer(): detect depth-zero enrollment candidates (R7-bounded), run each through the
|
|
9
|
+
* orchestrator (no operator mandate yet → consent-required), and emit ONE AGGREGATED consent
|
|
10
|
+
* item (P17 Bounded Notification Surface) — never one per machine. Never enrolls anything.
|
|
11
|
+
* - onMandateDelivered(): a portable account-follow-me mandate arrived from the operator machine
|
|
12
|
+
* → verify it (R4a bridge) → run the orchestrator with it → on allow, return an enroll-drive
|
|
13
|
+
* instruction for the caller to execute (per-server, OQ6). Still never self-authorizes.
|
|
14
|
+
*
|
|
15
|
+
* The service decides; the side effects (raising the attention item, driving the EnrollmentWizard,
|
|
16
|
+
* SubscriptionPool.add) are injected so each is independently testable + swappable. PR2 increment 3b.
|
|
17
|
+
*/
|
|
18
|
+
import { type PoolMachineDepth, type OperatorAccount } from './AccountFollowMeDetector.js';
|
|
19
|
+
import { AccountFollowMeOrchestrator, type FollowMeConsentRequest, type FollowMeMechanism } from './AccountFollowMeOrchestrator.js';
|
|
20
|
+
import { type PortableMandate } from '../coordination/AccountFollowMeMandateBridge.js';
|
|
21
|
+
import type crypto from 'node:crypto';
|
|
22
|
+
export interface AggregatedConsent {
|
|
23
|
+
/** Stable id so the attention item dedups per scan-set (one running item, not a flood). */
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
body: string;
|
|
27
|
+
priority: 'medium';
|
|
28
|
+
source: 'agent';
|
|
29
|
+
offers: FollowMeConsentRequest[];
|
|
30
|
+
}
|
|
31
|
+
export interface EnrollDriveInstruction {
|
|
32
|
+
accountId: string;
|
|
33
|
+
targetMachineId: string;
|
|
34
|
+
mechanism: FollowMeMechanism;
|
|
35
|
+
/** The verified mandate id authorizing this drive (audit). */
|
|
36
|
+
mandateId: string;
|
|
37
|
+
}
|
|
38
|
+
export interface AccountFollowMeServiceDeps {
|
|
39
|
+
/** Read the pool's per-machine account depth + the operator's accounts (WS5.1 / SubscriptionPool). */
|
|
40
|
+
readPoolDepth: () => {
|
|
41
|
+
machines: PoolMachineDepth[];
|
|
42
|
+
accounts: OperatorAccount[];
|
|
43
|
+
};
|
|
44
|
+
/** Per-account max-follow-machines cap (R7). */
|
|
45
|
+
maxFollowMachines: () => number;
|
|
46
|
+
/** (account,target) pairs already offered/in-flight — never re-offered (`${accountId}::${targetMachineId}`). */
|
|
47
|
+
inFlight: () => ReadonlySet<string>;
|
|
48
|
+
/** The §5.2 orchestrator (request-never-self-authorize). */
|
|
49
|
+
orchestrator: AccountFollowMeOrchestrator;
|
|
50
|
+
/** Raise ONE aggregated consent attention item. */
|
|
51
|
+
emitAggregatedConsent: (consent: AggregatedConsent) => void;
|
|
52
|
+
log?: (msg: string) => void;
|
|
53
|
+
}
|
|
54
|
+
export declare class AccountFollowMeService {
|
|
55
|
+
private readonly deps;
|
|
56
|
+
constructor(deps: AccountFollowMeServiceDeps);
|
|
57
|
+
/**
|
|
58
|
+
* §5.2 scan: surface (never act on) the depth-zero enrollment offers. Runs each candidate through
|
|
59
|
+
* the orchestrator — with no mandate yet, each yields consent-required — and emits ONE aggregated
|
|
60
|
+
* consent item over all of them. Returns the consent requests surfaced (empty if none).
|
|
61
|
+
*/
|
|
62
|
+
scanAndOffer(): {
|
|
63
|
+
offered: FollowMeConsentRequest[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A portable account-follow-me mandate was delivered from the operator machine. Verify it (R4a),
|
|
67
|
+
* then run the orchestrator with it. On allow, return an enroll-drive instruction; otherwise null
|
|
68
|
+
* (verification failed, or the gate denied — never proceed, never self-authorize).
|
|
69
|
+
*/
|
|
70
|
+
onMandateDelivered(args: {
|
|
71
|
+
portable: PortableMandate;
|
|
72
|
+
operatorEd25519PublicKey: crypto.KeyObject | string | Buffer;
|
|
73
|
+
expectedOperatorMachineFingerprint: string;
|
|
74
|
+
/** The (account,target,mechanism,email,nickname) this mandate is expected to authorize. */
|
|
75
|
+
request: {
|
|
76
|
+
accountId: string;
|
|
77
|
+
accountEmail: string;
|
|
78
|
+
targetMachineId: string;
|
|
79
|
+
targetMachineNickname: string;
|
|
80
|
+
mechanism?: FollowMeMechanism;
|
|
81
|
+
};
|
|
82
|
+
}): EnrollDriveInstruction | null;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=AccountFollowMeService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeService.d.ts","sourceRoot":"","sources":["../../src/core/AccountFollowMeService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAA0B,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AACnH,OAAO,EACL,2BAA2B,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACvB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,iDAAiD,CAAC;AAC/G,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,WAAW,iBAAiB;IAChC,2FAA2F;IAC3F,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,sBAAsB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,sGAAsG;IACtG,aAAa,EAAE,MAAM;QAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;QAAC,QAAQ,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC;IACnF,gDAAgD;IAChD,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,gHAAgH;IAChH,QAAQ,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,4DAA4D;IAC5D,YAAY,EAAE,2BAA2B,CAAC;IAC1C,mDAAmD;IACnD,qBAAqB,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC5D,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,qBAAa,sBAAsB;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,0BAA0B;IAE7D;;;;OAIG;IACH,YAAY,IAAI;QAAE,OAAO,EAAE,sBAAsB,EAAE,CAAA;KAAE;IAyCrD;;;;OAIG;IACH,kBAAkB,CAAC,IAAI,EAAE;QACvB,QAAQ,EAAE,eAAe,CAAC;QAC1B,wBAAwB,EAAE,MAAM,CAAC,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;QAC7D,kCAAkC,EAAE,MAAM,CAAC;QAC3C,2FAA2F;QAC3F,OAAO,EAAE;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAC;YAAC,eAAe,EAAE,MAAM,CAAC;YAAC,qBAAqB,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,iBAAiB,CAAA;SAAE,CAAC;KAC7I,GAAG,sBAAsB,GAAG,IAAI;CA6BlC"}
|