instar 1.3.611 → 1.3.613
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/core/AccountFollowMeRevocation.d.ts +178 -0
- package/dist/core/AccountFollowMeRevocation.d.ts.map +1 -0
- package/dist/core/AccountFollowMeRevocation.js +279 -0
- package/dist/core/AccountFollowMeRevocation.js.map +1 -0
- package/dist/core/resolveFollowMeEnrollTarget.d.ts +54 -0
- package/dist/core/resolveFollowMeEnrollTarget.d.ts.map +1 -0
- package/dist/core/resolveFollowMeEnrollTarget.js +57 -0
- package/dist/core/resolveFollowMeEnrollTarget.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +91 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.612.md +28 -0
- package/upgrades/1.3.613.md +22 -0
- package/upgrades/side-effects/ws52-account-follow-me-enroll-start.md +65 -0
- package/upgrades/side-effects/ws52-account-follow-me-revocation.md +122 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 R12 — Revocation / de-authorization data-plane executor (Mechanism B, the DEFAULT).
|
|
3
|
+
*
|
|
4
|
+
* "Stop following to machine X" is TWO things: a control-plane mandate revocation (PIN-gated on the
|
|
5
|
+
* Mandates tab; the agent CANNOT revoke — that authority stays with MandateGate) PLUS a real
|
|
6
|
+
* data-plane effect. This module is the data-plane half: it CONSUMES a revocation signal (the
|
|
7
|
+
* control-plane revoke already happened) and computes the honest data-plane outcome. It NEVER
|
|
8
|
+
* self-revokes the mandate and is NOT a new blocking authority — it is a deterministic planner +
|
|
9
|
+
* honest-state surface over INJECTED side-effect deps (logout, slot delete, SubscriptionPool.remove,
|
|
10
|
+
* attention emit, pending store). No direct I/O in the logic; every effect is injected so each is
|
|
11
|
+
* independently unit-testable (mirrors AccountFollowMeService / Grants / Orchestrator style).
|
|
12
|
+
*
|
|
13
|
+
* R12 has three honesty-load-bearing branches:
|
|
14
|
+
*
|
|
15
|
+
* (i) COOPERATIVE, still-paired target ONLINE (R12.i): the data-plane effect is local & total —
|
|
16
|
+
* the target logs the account OUT of its config-home, deletes the per-account slot, and
|
|
17
|
+
* SubscriptionPool.remove(accountId) fires on the target. End state: `removed`.
|
|
18
|
+
*
|
|
19
|
+
* (ii) DE-PAIRED / HOSTILE / non-cooperative holder (S8 / R12.i corrected): a B-minted credential
|
|
20
|
+
* is an independently-refreshable real OAuth login; instar CANNOT force the logout remotely.
|
|
21
|
+
* The HONEST revocation path is provider-side de-authorization. We surface a phone-first
|
|
22
|
+
* instruction ("machine X still holds its own live login for account Y — de-authorize/rotate
|
|
23
|
+
* at <provider> to kill it"), NEVER a false "removed everywhere". End state:
|
|
24
|
+
* `provider-rotation-required`. (When a machine de-pairs, MachineStatus flips to 'revoked' —
|
|
25
|
+
* that flip is the caller's; this module READS that status and refuses to claim a wipe.)
|
|
26
|
+
*
|
|
27
|
+
* (iii) OFFLINE-honest pending, BOUNDED (R12.iii): a still-paired target that is OFFLINE at revoke
|
|
28
|
+
* time gets its control-plane revoke immediately, but the data-plane wipe becomes a DURABLE
|
|
29
|
+
* pending action fired when it reconnects ("write it down durably, fire on return", the
|
|
30
|
+
* cross-machine-secret-sync pattern). Dashboard shows `revocation-pending`, NEVER `removed`.
|
|
31
|
+
* The pending wipe is NOT unbounded: after a fixed, operator-tunable reconnect-deadline it
|
|
32
|
+
* ESCALATES to a LOUD `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention
|
|
33
|
+
* item (resume-queue give-up discipline). Honest end state of an offline-forever / terminated
|
|
34
|
+
* VM is "rotate at provider", never a silently-aging "pending".
|
|
35
|
+
*
|
|
36
|
+
* FAIL CLOSED on uncertainty: if we cannot positively confirm the cooperative-online wipe path
|
|
37
|
+
* (unknown reachability, revoked status, the wipe effect threw), we NEVER report `removed` — we fall
|
|
38
|
+
* to pending or provider-rotation-required. Deny-by-default for the optimistic "removed" claim.
|
|
39
|
+
*
|
|
40
|
+
* Mechanism A (sealed-transport) is dark/refused-for-Anthropic; per R9 it is out of scope here
|
|
41
|
+
* beyond the minimal `mechanism` discriminator — a Mechanism-A revoke ALSO requires the wipe verb +
|
|
42
|
+
* an unconditional provider-rotation prompt (a delivered credential cannot be un-delivered), so this
|
|
43
|
+
* module flags `providerRotationRequired: true` for any Mechanism-A revoke regardless of branch.
|
|
44
|
+
*/
|
|
45
|
+
export type RevocationMechanism = 're-mint' | 'credential-transport';
|
|
46
|
+
/** Target reachability/cooperation posture at revoke time — drives the R12 branch. */
|
|
47
|
+
export type TargetPosture = 'cooperative-online' | 'offline' | 'revoked';
|
|
48
|
+
/** Honest end-state of a revocation's DATA plane (never conflate with the control-plane revoke). */
|
|
49
|
+
export type RevocationDataState = 'removed' | 'revocation-pending' | 'revocation-failed' | 'provider-rotation-required';
|
|
50
|
+
export interface RevocationRequest {
|
|
51
|
+
accountId: string;
|
|
52
|
+
/** Operator-facing account email (shown in honest operator messaging). */
|
|
53
|
+
accountEmail: string;
|
|
54
|
+
targetMachineId: string;
|
|
55
|
+
/** Operator-facing nickname of the target machine (shown in honest operator messaging). */
|
|
56
|
+
targetMachineNickname: string;
|
|
57
|
+
/** Provider name for the provider-side de-authorization instruction (e.g. 'Anthropic'). */
|
|
58
|
+
provider: string;
|
|
59
|
+
/** The mandate id whose control-plane revoke triggered this data-plane effect (audit). */
|
|
60
|
+
mandateId: string;
|
|
61
|
+
/** Default 're-mint' (Mechanism B). 'credential-transport' ⇒ unconditional provider rotation. */
|
|
62
|
+
mechanism?: RevocationMechanism;
|
|
63
|
+
}
|
|
64
|
+
/** A phone-first provider-side de-authorization instruction — a real instruction, NOT a "handled" claim. */
|
|
65
|
+
export interface ProviderRotationInstruction {
|
|
66
|
+
kind: 'provider-rotation-required';
|
|
67
|
+
accountId: string;
|
|
68
|
+
accountEmail: string;
|
|
69
|
+
targetMachineId: string;
|
|
70
|
+
targetMachineNickname: string;
|
|
71
|
+
provider: string;
|
|
72
|
+
/** Plain-English, honest message surfaced to the operator (never implies instar removed it). */
|
|
73
|
+
message: string;
|
|
74
|
+
}
|
|
75
|
+
/** A LOUD aggregated attention item for an offline target that gave up (R12.iii give-up). */
|
|
76
|
+
export interface RevocationFailedAttention {
|
|
77
|
+
/** Stable id so the item dedups per (account,target) — one running item, not a flood (P17). */
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
body: string;
|
|
81
|
+
priority: 'high';
|
|
82
|
+
source: 'agent';
|
|
83
|
+
}
|
|
84
|
+
/** A durable pending-wipe record — fired on the target's reconnect; bounded by the deadline. */
|
|
85
|
+
export interface PendingWipeRecord {
|
|
86
|
+
accountId: string;
|
|
87
|
+
targetMachineId: string;
|
|
88
|
+
mandateId: string;
|
|
89
|
+
provider: string;
|
|
90
|
+
accountEmail: string;
|
|
91
|
+
targetMachineNickname: string;
|
|
92
|
+
mechanism: RevocationMechanism;
|
|
93
|
+
/** When the control-plane revoke fired — the deadline clock starts here. */
|
|
94
|
+
revokedAt: number;
|
|
95
|
+
/** Absolute deadline; after this a still-pending wipe escalates to revocation-failed. */
|
|
96
|
+
deadlineAt: number;
|
|
97
|
+
}
|
|
98
|
+
/** Durable persistence seam for pending wipes (production: JSON/SQLite; tests: in-memory). */
|
|
99
|
+
export interface PendingWipeStore {
|
|
100
|
+
/** Upsert keyed on `${accountId}::${targetMachineId}`. */
|
|
101
|
+
put(record: PendingWipeRecord): void;
|
|
102
|
+
/** Resolve (the wipe fired) — remove the pending record. */
|
|
103
|
+
remove(accountId: string, targetMachineId: string): void;
|
|
104
|
+
get(accountId: string, targetMachineId: string): PendingWipeRecord | undefined;
|
|
105
|
+
all(): PendingWipeRecord[];
|
|
106
|
+
}
|
|
107
|
+
export declare function inMemoryPendingWipeStore(): PendingWipeStore;
|
|
108
|
+
/** Outcome of an attempted cooperative-online data-plane wipe (each step injected). */
|
|
109
|
+
export interface CooperativeWipeResult {
|
|
110
|
+
loggedOut: boolean;
|
|
111
|
+
slotDeleted: boolean;
|
|
112
|
+
poolRemoved: boolean;
|
|
113
|
+
}
|
|
114
|
+
export interface AccountFollowMeRevocationDeps {
|
|
115
|
+
/** Master gate: the whole feature is dark unless this is true (single-machine / flag-off ⇒ no-op). */
|
|
116
|
+
enabled: () => boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Execute the cooperative data-plane wipe ON the (online, still-paired) target: framework logout
|
|
119
|
+
* against that CLAUDE_CONFIG_DIR + delete the per-account slot + SubscriptionPool.remove(accountId).
|
|
120
|
+
* MUST throw or return all-false on any failure — we then fail closed (never claim `removed`).
|
|
121
|
+
*/
|
|
122
|
+
cooperativeWipe: (req: RevocationRequest) => CooperativeWipeResult;
|
|
123
|
+
/** Durable pending-wipe ledger (offline path). */
|
|
124
|
+
pendingStore: PendingWipeStore;
|
|
125
|
+
/** Raise ONE aggregated LOUD attention item (offline give-up). */
|
|
126
|
+
emitRevocationFailed: (item: RevocationFailedAttention) => void;
|
|
127
|
+
/** Reconnect-deadline before an offline pending wipe escalates to revocation-failed (ms). */
|
|
128
|
+
reconnectDeadlineMs: () => number;
|
|
129
|
+
now?: () => number;
|
|
130
|
+
log?: (msg: string) => void;
|
|
131
|
+
}
|
|
132
|
+
export type RevocationOutcome = {
|
|
133
|
+
state: RevocationDataState;
|
|
134
|
+
accountId: string;
|
|
135
|
+
targetMachineId: string;
|
|
136
|
+
/** True whenever provider-side rotation is the (only) complete revocation — caller surfaces it. */
|
|
137
|
+
providerRotationRequired: boolean;
|
|
138
|
+
/** Present for the de-paired/hostile and Mechanism-A paths — a real phone-first instruction. */
|
|
139
|
+
providerRotation?: ProviderRotationInstruction;
|
|
140
|
+
/** Present on the cooperative-online success path. */
|
|
141
|
+
wipe?: CooperativeWipeResult;
|
|
142
|
+
reason: string;
|
|
143
|
+
};
|
|
144
|
+
export declare class AccountFollowMeRevocation {
|
|
145
|
+
private readonly deps;
|
|
146
|
+
private readonly now;
|
|
147
|
+
constructor(deps: AccountFollowMeRevocationDeps);
|
|
148
|
+
private rotationInstruction;
|
|
149
|
+
/**
|
|
150
|
+
* Revoke account access on `targetMachineId` given the control-plane revoke already fired. The
|
|
151
|
+
* `posture` (computed by the caller from authoritative registry state — online flag, MachineStatus)
|
|
152
|
+
* selects the R12 branch. Returns the HONEST data-plane outcome; never claims `removed` it can't
|
|
153
|
+
* confirm. A no-op (`provider-rotation-required` with a no-op message) when the feature is dark.
|
|
154
|
+
*/
|
|
155
|
+
revoke(req: RevocationRequest, posture: TargetPosture): RevocationOutcome;
|
|
156
|
+
/**
|
|
157
|
+
* A previously-offline target reconnected — fire its durable pending wipe (R12.iii). Drives the
|
|
158
|
+
* SAME cooperative wipe as the online path; on success the pending record is cleared and we report
|
|
159
|
+
* `removed`. On failure/partial we keep the record (it may still escalate at the deadline).
|
|
160
|
+
* A no-op if no pending record exists for (account, target) or the feature is dark.
|
|
161
|
+
*/
|
|
162
|
+
onTargetReconnect(accountId: string, targetMachineId: string): RevocationOutcome | null;
|
|
163
|
+
/**
|
|
164
|
+
* Sweep durable pending wipes whose reconnect-deadline has passed and escalate each to a LOUD
|
|
165
|
+
* `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention item (R12.iii give-up).
|
|
166
|
+
* Each escalated record is removed from the pending store (its honest end-state is now provider
|
|
167
|
+
* rotation, never a silently-aging "pending"). Returns the records that escalated. Caller runs
|
|
168
|
+
* this on a cadence (resume-queue drainer style). No-op when the feature is dark.
|
|
169
|
+
*/
|
|
170
|
+
sweepDeadlines(): RevocationFailedAttention[];
|
|
171
|
+
/**
|
|
172
|
+
* Honest dashboard state for an (account, target) pair: a live pending record reports
|
|
173
|
+
* `revocation-pending` (or `revocation-failed` once past its deadline), else null (no pending
|
|
174
|
+
* revocation in flight). Read surface — never mutates.
|
|
175
|
+
*/
|
|
176
|
+
pendingStateFor(accountId: string, targetMachineId: string): RevocationDataState | null;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=AccountFollowMeRevocation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeRevocation.d.ts","sourceRoot":"","sources":["../../src/core/AccountFollowMeRevocation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,sBAAsB,CAAC;AAErE,sFAAsF;AACtF,MAAM,MAAM,aAAa,GACrB,oBAAoB,GACpB,SAAS,GACT,SAAS,CAAC;AAEd,oGAAoG;AACpG,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,oBAAoB,GACpB,mBAAmB,GACnB,4BAA4B,CAAC;AAEjC,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,2FAA2F;IAC3F,qBAAqB,EAAE,MAAM,CAAC;IAC9B,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,SAAS,CAAC,EAAE,mBAAmB,CAAC;CACjC;AAED,4GAA4G;AAC5G,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,4BAA4B,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,gGAAgG;IAChG,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,6FAA6F;AAC7F,MAAM,WAAW,yBAAyB;IACxC,+FAA+F;IAC/F,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,gGAAgG;AAChG,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,SAAS,EAAE,mBAAmB,CAAC;IAC/B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,8FAA8F;AAC9F,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACrC,4DAA4D;IAC5D,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IACzD,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAAC;IAC/E,GAAG,IAAI,iBAAiB,EAAE,CAAC;CAC5B;AAED,wBAAgB,wBAAwB,IAAI,gBAAgB,CAS3D;AAED,uFAAuF;AACvF,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,sGAAsG;IACtG,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB;;;;OAIG;IACH,eAAe,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,qBAAqB,CAAC;IACnE,kDAAkD;IAClD,YAAY,EAAE,gBAAgB,CAAC;IAC/B,kEAAkE;IAClE,oBAAoB,EAAE,CAAC,IAAI,EAAE,yBAAyB,KAAK,IAAI,CAAC;IAChE,6FAA6F;IAC7F,mBAAmB,EAAE,MAAM,MAAM,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,mGAAmG;IACnG,wBAAwB,EAAE,OAAO,CAAC;IAClC,gGAAgG;IAChG,gBAAgB,CAAC,EAAE,2BAA2B,CAAC;IAC/C,sDAAsD;IACtD,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qBAAa,yBAAyB;IAExB,OAAO,CAAC,QAAQ,CAAC,IAAI;IADjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBACN,IAAI,EAAE,6BAA6B;IAIhE,OAAO,CAAC,mBAAmB;IAe3B;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,iBAAiB,EAAE,OAAO,EAAE,aAAa,GAAG,iBAAiB;IAmJzE;;;;;OAKG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAiBvF;;;;;;OAMG;IACH,cAAc,IAAI,yBAAyB,EAAE;IA2B7C;;;;OAIG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;CAKxF"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 R12 — Revocation / de-authorization data-plane executor (Mechanism B, the DEFAULT).
|
|
3
|
+
*
|
|
4
|
+
* "Stop following to machine X" is TWO things: a control-plane mandate revocation (PIN-gated on the
|
|
5
|
+
* Mandates tab; the agent CANNOT revoke — that authority stays with MandateGate) PLUS a real
|
|
6
|
+
* data-plane effect. This module is the data-plane half: it CONSUMES a revocation signal (the
|
|
7
|
+
* control-plane revoke already happened) and computes the honest data-plane outcome. It NEVER
|
|
8
|
+
* self-revokes the mandate and is NOT a new blocking authority — it is a deterministic planner +
|
|
9
|
+
* honest-state surface over INJECTED side-effect deps (logout, slot delete, SubscriptionPool.remove,
|
|
10
|
+
* attention emit, pending store). No direct I/O in the logic; every effect is injected so each is
|
|
11
|
+
* independently unit-testable (mirrors AccountFollowMeService / Grants / Orchestrator style).
|
|
12
|
+
*
|
|
13
|
+
* R12 has three honesty-load-bearing branches:
|
|
14
|
+
*
|
|
15
|
+
* (i) COOPERATIVE, still-paired target ONLINE (R12.i): the data-plane effect is local & total —
|
|
16
|
+
* the target logs the account OUT of its config-home, deletes the per-account slot, and
|
|
17
|
+
* SubscriptionPool.remove(accountId) fires on the target. End state: `removed`.
|
|
18
|
+
*
|
|
19
|
+
* (ii) DE-PAIRED / HOSTILE / non-cooperative holder (S8 / R12.i corrected): a B-minted credential
|
|
20
|
+
* is an independently-refreshable real OAuth login; instar CANNOT force the logout remotely.
|
|
21
|
+
* The HONEST revocation path is provider-side de-authorization. We surface a phone-first
|
|
22
|
+
* instruction ("machine X still holds its own live login for account Y — de-authorize/rotate
|
|
23
|
+
* at <provider> to kill it"), NEVER a false "removed everywhere". End state:
|
|
24
|
+
* `provider-rotation-required`. (When a machine de-pairs, MachineStatus flips to 'revoked' —
|
|
25
|
+
* that flip is the caller's; this module READS that status and refuses to claim a wipe.)
|
|
26
|
+
*
|
|
27
|
+
* (iii) OFFLINE-honest pending, BOUNDED (R12.iii): a still-paired target that is OFFLINE at revoke
|
|
28
|
+
* time gets its control-plane revoke immediately, but the data-plane wipe becomes a DURABLE
|
|
29
|
+
* pending action fired when it reconnects ("write it down durably, fire on return", the
|
|
30
|
+
* cross-machine-secret-sync pattern). Dashboard shows `revocation-pending`, NEVER `removed`.
|
|
31
|
+
* The pending wipe is NOT unbounded: after a fixed, operator-tunable reconnect-deadline it
|
|
32
|
+
* ESCALATES to a LOUD `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention
|
|
33
|
+
* item (resume-queue give-up discipline). Honest end state of an offline-forever / terminated
|
|
34
|
+
* VM is "rotate at provider", never a silently-aging "pending".
|
|
35
|
+
*
|
|
36
|
+
* FAIL CLOSED on uncertainty: if we cannot positively confirm the cooperative-online wipe path
|
|
37
|
+
* (unknown reachability, revoked status, the wipe effect threw), we NEVER report `removed` — we fall
|
|
38
|
+
* to pending or provider-rotation-required. Deny-by-default for the optimistic "removed" claim.
|
|
39
|
+
*
|
|
40
|
+
* Mechanism A (sealed-transport) is dark/refused-for-Anthropic; per R9 it is out of scope here
|
|
41
|
+
* beyond the minimal `mechanism` discriminator — a Mechanism-A revoke ALSO requires the wipe verb +
|
|
42
|
+
* an unconditional provider-rotation prompt (a delivered credential cannot be un-delivered), so this
|
|
43
|
+
* module flags `providerRotationRequired: true` for any Mechanism-A revoke regardless of branch.
|
|
44
|
+
*/
|
|
45
|
+
export function inMemoryPendingWipeStore() {
|
|
46
|
+
const map = new Map();
|
|
47
|
+
const key = (a, t) => `${a}::${t}`;
|
|
48
|
+
return {
|
|
49
|
+
put: (r) => { map.set(key(r.accountId, r.targetMachineId), r); },
|
|
50
|
+
remove: (a, t) => { map.delete(key(a, t)); },
|
|
51
|
+
get: (a, t) => map.get(key(a, t)),
|
|
52
|
+
all: () => [...map.values()],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export class AccountFollowMeRevocation {
|
|
56
|
+
deps;
|
|
57
|
+
now;
|
|
58
|
+
constructor(deps) {
|
|
59
|
+
this.deps = deps;
|
|
60
|
+
this.now = deps.now ?? Date.now;
|
|
61
|
+
}
|
|
62
|
+
rotationInstruction(req, lead) {
|
|
63
|
+
return {
|
|
64
|
+
kind: 'provider-rotation-required',
|
|
65
|
+
accountId: req.accountId,
|
|
66
|
+
accountEmail: req.accountEmail,
|
|
67
|
+
targetMachineId: req.targetMachineId,
|
|
68
|
+
targetMachineNickname: req.targetMachineNickname,
|
|
69
|
+
provider: req.provider,
|
|
70
|
+
message: `${lead} "${req.targetMachineNickname}" still holds its own live, refreshable login for ` +
|
|
71
|
+
`${req.accountEmail}. instar cannot revoke it remotely — de-authorize that session/device ` +
|
|
72
|
+
`or rotate the credential at ${req.provider} to kill it.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Revoke account access on `targetMachineId` given the control-plane revoke already fired. The
|
|
77
|
+
* `posture` (computed by the caller from authoritative registry state — online flag, MachineStatus)
|
|
78
|
+
* selects the R12 branch. Returns the HONEST data-plane outcome; never claims `removed` it can't
|
|
79
|
+
* confirm. A no-op (`provider-rotation-required` with a no-op message) when the feature is dark.
|
|
80
|
+
*/
|
|
81
|
+
revoke(req, posture) {
|
|
82
|
+
const mechanism = req.mechanism ?? 're-mint';
|
|
83
|
+
// Dark / single-machine ⇒ strict no-op (deny-by-default; never act).
|
|
84
|
+
if (!this.deps.enabled()) {
|
|
85
|
+
return {
|
|
86
|
+
state: 'provider-rotation-required',
|
|
87
|
+
accountId: req.accountId,
|
|
88
|
+
targetMachineId: req.targetMachineId,
|
|
89
|
+
providerRotationRequired: false,
|
|
90
|
+
reason: 'feature-disabled',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Mechanism A relocated a real credential — provider rotation is ALWAYS required regardless of
|
|
94
|
+
// branch (a delivered credential cannot be un-delivered, R12.ii). We still attempt the wipe for
|
|
95
|
+
// a cooperative-online A target, but the rotation instruction is unconditional.
|
|
96
|
+
const mechARotation = mechanism === 'credential-transport';
|
|
97
|
+
// (ii) De-paired / hostile / MachineStatus==='revoked' — provider-side ONLY (S8/R12.i corrected).
|
|
98
|
+
if (posture === 'revoked') {
|
|
99
|
+
const rotation = this.rotationInstruction(req, 'Machine');
|
|
100
|
+
this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: de-paired/hostile — ` +
|
|
101
|
+
`provider-side de-authorization required (no remote wipe possible)`);
|
|
102
|
+
return {
|
|
103
|
+
state: 'provider-rotation-required',
|
|
104
|
+
accountId: req.accountId,
|
|
105
|
+
targetMachineId: req.targetMachineId,
|
|
106
|
+
providerRotationRequired: true,
|
|
107
|
+
providerRotation: rotation,
|
|
108
|
+
reason: 'de-paired-non-cooperative',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// (iii) Offline still-paired target — durable pending wipe, fired on reconnect (BOUNDED).
|
|
112
|
+
if (posture === 'offline') {
|
|
113
|
+
const t = this.now();
|
|
114
|
+
const record = {
|
|
115
|
+
accountId: req.accountId,
|
|
116
|
+
targetMachineId: req.targetMachineId,
|
|
117
|
+
mandateId: req.mandateId,
|
|
118
|
+
provider: req.provider,
|
|
119
|
+
accountEmail: req.accountEmail,
|
|
120
|
+
targetMachineNickname: req.targetMachineNickname,
|
|
121
|
+
mechanism,
|
|
122
|
+
revokedAt: t,
|
|
123
|
+
deadlineAt: t + this.deps.reconnectDeadlineMs(),
|
|
124
|
+
};
|
|
125
|
+
this.deps.pendingStore.put(record);
|
|
126
|
+
this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: target offline — ` +
|
|
127
|
+
`durable pending wipe (deadline +${this.deps.reconnectDeadlineMs()}ms)`);
|
|
128
|
+
return {
|
|
129
|
+
state: 'revocation-pending',
|
|
130
|
+
accountId: req.accountId,
|
|
131
|
+
targetMachineId: req.targetMachineId,
|
|
132
|
+
// For Mechanism A, rotate at provider NOW even while pending (the blob already landed).
|
|
133
|
+
providerRotationRequired: mechARotation,
|
|
134
|
+
providerRotation: mechARotation
|
|
135
|
+
? this.rotationInstruction(req, 'Offline machine')
|
|
136
|
+
: undefined,
|
|
137
|
+
reason: 'target-offline-pending',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// (i) Cooperative, still-paired, ONLINE target — local & total wipe. Fail closed on any error.
|
|
141
|
+
let wipe;
|
|
142
|
+
try {
|
|
143
|
+
wipe = this.deps.cooperativeWipe(req);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
// The wipe threw — we CANNOT claim removed. Fail closed to a durable pending retry.
|
|
147
|
+
const t = this.now();
|
|
148
|
+
this.deps.pendingStore.put({
|
|
149
|
+
accountId: req.accountId,
|
|
150
|
+
targetMachineId: req.targetMachineId,
|
|
151
|
+
mandateId: req.mandateId,
|
|
152
|
+
provider: req.provider,
|
|
153
|
+
accountEmail: req.accountEmail,
|
|
154
|
+
targetMachineNickname: req.targetMachineNickname,
|
|
155
|
+
mechanism,
|
|
156
|
+
revokedAt: t,
|
|
157
|
+
deadlineAt: t + this.deps.reconnectDeadlineMs(),
|
|
158
|
+
});
|
|
159
|
+
this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: cooperative wipe THREW ` +
|
|
160
|
+
`(${err instanceof Error ? err.message : String(err)}) — fail-closed to pending`);
|
|
161
|
+
return {
|
|
162
|
+
state: 'revocation-pending',
|
|
163
|
+
accountId: req.accountId,
|
|
164
|
+
targetMachineId: req.targetMachineId,
|
|
165
|
+
providerRotationRequired: mechARotation,
|
|
166
|
+
providerRotation: mechARotation ? this.rotationInstruction(req, 'Machine') : undefined,
|
|
167
|
+
reason: 'cooperative-wipe-error',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const fullyWiped = wipe.loggedOut && wipe.slotDeleted && wipe.poolRemoved;
|
|
171
|
+
if (!fullyWiped) {
|
|
172
|
+
// Partial wipe — fail closed: do NOT claim removed; keep a durable pending so it retries.
|
|
173
|
+
const t = this.now();
|
|
174
|
+
this.deps.pendingStore.put({
|
|
175
|
+
accountId: req.accountId,
|
|
176
|
+
targetMachineId: req.targetMachineId,
|
|
177
|
+
mandateId: req.mandateId,
|
|
178
|
+
provider: req.provider,
|
|
179
|
+
accountEmail: req.accountEmail,
|
|
180
|
+
targetMachineNickname: req.targetMachineNickname,
|
|
181
|
+
mechanism,
|
|
182
|
+
revokedAt: t,
|
|
183
|
+
deadlineAt: t + this.deps.reconnectDeadlineMs(),
|
|
184
|
+
});
|
|
185
|
+
this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: partial wipe ` +
|
|
186
|
+
`(logout=${wipe.loggedOut} slot=${wipe.slotDeleted} pool=${wipe.poolRemoved}) — pending`);
|
|
187
|
+
return {
|
|
188
|
+
state: 'revocation-pending',
|
|
189
|
+
accountId: req.accountId,
|
|
190
|
+
targetMachineId: req.targetMachineId,
|
|
191
|
+
providerRotationRequired: mechARotation,
|
|
192
|
+
providerRotation: mechARotation ? this.rotationInstruction(req, 'Machine') : undefined,
|
|
193
|
+
wipe,
|
|
194
|
+
reason: 'cooperative-wipe-partial',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Confirmed local & total. The pending record (if any earlier offline/error attempt) is cleared.
|
|
198
|
+
this.deps.pendingStore.remove(req.accountId, req.targetMachineId);
|
|
199
|
+
this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: removed (logout + slot + pool)`);
|
|
200
|
+
return {
|
|
201
|
+
state: mechARotation ? 'provider-rotation-required' : 'removed',
|
|
202
|
+
accountId: req.accountId,
|
|
203
|
+
targetMachineId: req.targetMachineId,
|
|
204
|
+
// Even a clean Mechanism-A wipe still requires provider rotation (blob was delivered).
|
|
205
|
+
providerRotationRequired: mechARotation,
|
|
206
|
+
providerRotation: mechARotation ? this.rotationInstruction(req, 'Machine') : undefined,
|
|
207
|
+
wipe,
|
|
208
|
+
reason: mechARotation ? 'wiped-but-mechanism-a-rotate' : 'wiped-local-and-total',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* A previously-offline target reconnected — fire its durable pending wipe (R12.iii). Drives the
|
|
213
|
+
* SAME cooperative wipe as the online path; on success the pending record is cleared and we report
|
|
214
|
+
* `removed`. On failure/partial we keep the record (it may still escalate at the deadline).
|
|
215
|
+
* A no-op if no pending record exists for (account, target) or the feature is dark.
|
|
216
|
+
*/
|
|
217
|
+
onTargetReconnect(accountId, targetMachineId) {
|
|
218
|
+
if (!this.deps.enabled())
|
|
219
|
+
return null;
|
|
220
|
+
const record = this.deps.pendingStore.get(accountId, targetMachineId);
|
|
221
|
+
if (!record)
|
|
222
|
+
return null;
|
|
223
|
+
const req = {
|
|
224
|
+
accountId: record.accountId,
|
|
225
|
+
accountEmail: record.accountEmail,
|
|
226
|
+
targetMachineId: record.targetMachineId,
|
|
227
|
+
targetMachineNickname: record.targetMachineNickname,
|
|
228
|
+
provider: record.provider,
|
|
229
|
+
mandateId: record.mandateId,
|
|
230
|
+
mechanism: record.mechanism,
|
|
231
|
+
};
|
|
232
|
+
// Reconnected ⇒ treat as cooperative-online and run the same wipe (revoke clears the record on success).
|
|
233
|
+
return this.revoke(req, 'cooperative-online');
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Sweep durable pending wipes whose reconnect-deadline has passed and escalate each to a LOUD
|
|
237
|
+
* `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention item (R12.iii give-up).
|
|
238
|
+
* Each escalated record is removed from the pending store (its honest end-state is now provider
|
|
239
|
+
* rotation, never a silently-aging "pending"). Returns the records that escalated. Caller runs
|
|
240
|
+
* this on a cadence (resume-queue drainer style). No-op when the feature is dark.
|
|
241
|
+
*/
|
|
242
|
+
sweepDeadlines() {
|
|
243
|
+
if (!this.deps.enabled())
|
|
244
|
+
return [];
|
|
245
|
+
const t = this.now();
|
|
246
|
+
const escalated = [];
|
|
247
|
+
for (const record of this.deps.pendingStore.all()) {
|
|
248
|
+
if (record.deadlineAt > t)
|
|
249
|
+
continue; // still within the deadline — stays pending.
|
|
250
|
+
const item = {
|
|
251
|
+
id: `agent:account-follow-me-revoke-failed:${record.accountId}::${record.targetMachineId}`,
|
|
252
|
+
title: `Revocation FAILED on "${record.targetMachineNickname}" — rotate at ${record.provider} NOW`,
|
|
253
|
+
body: `I revoked access for ${record.accountEmail} on "${record.targetMachineNickname}", but that ` +
|
|
254
|
+
`machine never reconnected to confirm its copy was destroyed. Its login may still be live. ` +
|
|
255
|
+
`Rotate or de-authorize the credential at ${record.provider} now to be certain.`,
|
|
256
|
+
priority: 'high',
|
|
257
|
+
source: 'agent',
|
|
258
|
+
};
|
|
259
|
+
this.deps.emitRevocationFailed(item);
|
|
260
|
+
this.deps.pendingStore.remove(record.accountId, record.targetMachineId);
|
|
261
|
+
this.deps.log?.(`[account-follow-me] revoke ${record.accountId}→${record.targetMachineId}: pending wipe ` +
|
|
262
|
+
`EXCEEDED deadline — escalated to revocation-FAILED (rotate at ${record.provider})`);
|
|
263
|
+
escalated.push(item);
|
|
264
|
+
}
|
|
265
|
+
return escalated;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Honest dashboard state for an (account, target) pair: a live pending record reports
|
|
269
|
+
* `revocation-pending` (or `revocation-failed` once past its deadline), else null (no pending
|
|
270
|
+
* revocation in flight). Read surface — never mutates.
|
|
271
|
+
*/
|
|
272
|
+
pendingStateFor(accountId, targetMachineId) {
|
|
273
|
+
const record = this.deps.pendingStore.get(accountId, targetMachineId);
|
|
274
|
+
if (!record)
|
|
275
|
+
return null;
|
|
276
|
+
return record.deadlineAt > this.now() ? 'revocation-pending' : 'revocation-failed';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=AccountFollowMeRevocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccountFollowMeRevocation.js","sourceRoot":"","sources":["../../src/core/AccountFollowMeRevocation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.3 / S7 — resolve the OPERATOR-APPROVED enrollment target for an account, AUTHORITATIVELY,
|
|
3
|
+
* from real pool state — NEVER from the request body. This is the keystone of the email-safety gate:
|
|
4
|
+
* `expectedEmail` MUST be what the operator approved (the account's known email across the mesh), so
|
|
5
|
+
* `completeFollowMe` can later validate the freshly-minted login against it. A self-asserted email in
|
|
6
|
+
* the request body would defeat the gate entirely (S7), so this function only ever reads from the same
|
|
7
|
+
* authoritative source the scan uses: the local SubscriptionPool + the per-peer pool views (which
|
|
8
|
+
* carry each account's id + email from the replicated `subscription-account-meta` projection).
|
|
9
|
+
*
|
|
10
|
+
* FAIL-CLOSED by construction: if the account's email cannot be resolved from any holder, this returns
|
|
11
|
+
* `{ resolved: false }` and the caller MUST refuse (409) rather than starting an enrollment with a
|
|
12
|
+
* blank/wrong expectedEmail. Pure (no I/O) ⇒ unit-testable; the route supplies the fetched views.
|
|
13
|
+
*/
|
|
14
|
+
import type { MachinePoolView } from './accountFollowMeDepth.js';
|
|
15
|
+
/** A local SubscriptionPool account row, as the resolver needs it. */
|
|
16
|
+
export interface LocalAccountRow {
|
|
17
|
+
id: string;
|
|
18
|
+
email?: string;
|
|
19
|
+
nickname?: string;
|
|
20
|
+
provider?: string;
|
|
21
|
+
framework?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ResolveFollowMeEnrollTargetInput {
|
|
24
|
+
accountId: string;
|
|
25
|
+
/** This machine's local SubscriptionPool accounts (authoritative when the account is held here). */
|
|
26
|
+
localAccounts: LocalAccountRow[];
|
|
27
|
+
/** Cross-machine per-peer pool views (the same source the scan uses — carries id + email). */
|
|
28
|
+
peerViews: MachinePoolView[];
|
|
29
|
+
/** Fallbacks when the meta does not carry provider/framework (it usually does). */
|
|
30
|
+
defaultProvider?: string;
|
|
31
|
+
defaultFramework?: string;
|
|
32
|
+
}
|
|
33
|
+
export type ResolveFollowMeEnrollTargetResult = {
|
|
34
|
+
resolved: true;
|
|
35
|
+
/** The OPERATOR-APPROVED account email (authoritative; never from the request body). */
|
|
36
|
+
expectedEmail: string;
|
|
37
|
+
provider: string;
|
|
38
|
+
framework: string;
|
|
39
|
+
/** Operator-facing label for the new pending login. */
|
|
40
|
+
label: string;
|
|
41
|
+
} | {
|
|
42
|
+
resolved: false;
|
|
43
|
+
reason: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the approved email + provider/framework + label for `accountId`.
|
|
47
|
+
*
|
|
48
|
+
* Resolution order (authoritative-first):
|
|
49
|
+
* 1. the LOCAL SubscriptionPool (if this machine already holds/knows the account) — most trustworthy;
|
|
50
|
+
* 2. any PEER pool view that reports the account with a non-empty email (the replicated meta).
|
|
51
|
+
* The email MUST be a non-empty string for the result to be `resolved` — otherwise fail-closed.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveFollowMeEnrollTarget(input: ResolveFollowMeEnrollTargetInput): ResolveFollowMeEnrollTargetResult;
|
|
54
|
+
//# sourceMappingURL=resolveFollowMeEnrollTarget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveFollowMeEnrollTarget.d.ts","sourceRoot":"","sources":["../../src/core/resolveFollowMeEnrollTarget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gCAAgC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,aAAa,EAAE,eAAe,EAAE,CAAC;IACjC,8FAA8F;IAC9F,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,iCAAiC,GACzC;IACE,QAAQ,EAAE,IAAI,CAAC;IACf,wFAAwF;IACxF,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gCAAgC,GACtC,iCAAiC,CAoCnC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS5.2 §5.3 / S7 — resolve the OPERATOR-APPROVED enrollment target for an account, AUTHORITATIVELY,
|
|
3
|
+
* from real pool state — NEVER from the request body. This is the keystone of the email-safety gate:
|
|
4
|
+
* `expectedEmail` MUST be what the operator approved (the account's known email across the mesh), so
|
|
5
|
+
* `completeFollowMe` can later validate the freshly-minted login against it. A self-asserted email in
|
|
6
|
+
* the request body would defeat the gate entirely (S7), so this function only ever reads from the same
|
|
7
|
+
* authoritative source the scan uses: the local SubscriptionPool + the per-peer pool views (which
|
|
8
|
+
* carry each account's id + email from the replicated `subscription-account-meta` projection).
|
|
9
|
+
*
|
|
10
|
+
* FAIL-CLOSED by construction: if the account's email cannot be resolved from any holder, this returns
|
|
11
|
+
* `{ resolved: false }` and the caller MUST refuse (409) rather than starting an enrollment with a
|
|
12
|
+
* blank/wrong expectedEmail. Pure (no I/O) ⇒ unit-testable; the route supplies the fetched views.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the approved email + provider/framework + label for `accountId`.
|
|
16
|
+
*
|
|
17
|
+
* Resolution order (authoritative-first):
|
|
18
|
+
* 1. the LOCAL SubscriptionPool (if this machine already holds/knows the account) — most trustworthy;
|
|
19
|
+
* 2. any PEER pool view that reports the account with a non-empty email (the replicated meta).
|
|
20
|
+
* The email MUST be a non-empty string for the result to be `resolved` — otherwise fail-closed.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveFollowMeEnrollTarget(input) {
|
|
23
|
+
const { accountId } = input;
|
|
24
|
+
const defaultProvider = input.defaultProvider ?? 'anthropic';
|
|
25
|
+
const defaultFramework = input.defaultFramework ?? 'claude-code';
|
|
26
|
+
// 1) Local pool — most authoritative when present.
|
|
27
|
+
const local = input.localAccounts.find((a) => a.id === accountId);
|
|
28
|
+
if (local && typeof local.email === 'string' && local.email.trim().length > 0) {
|
|
29
|
+
return {
|
|
30
|
+
resolved: true,
|
|
31
|
+
expectedEmail: local.email.trim(),
|
|
32
|
+
provider: (local.provider && local.provider.length > 0) ? local.provider : defaultProvider,
|
|
33
|
+
framework: (local.framework && local.framework.length > 0) ? local.framework : defaultFramework,
|
|
34
|
+
label: (local.nickname && local.nickname.trim().length > 0) ? local.nickname.trim() : accountId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// 2) Peer views — the replicated meta projection (carries id + email).
|
|
38
|
+
for (const view of input.peerViews) {
|
|
39
|
+
for (const row of view.accounts) {
|
|
40
|
+
if (row.accountId !== accountId)
|
|
41
|
+
continue;
|
|
42
|
+
if (typeof row.email === 'string' && row.email.trim().length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
resolved: true,
|
|
45
|
+
expectedEmail: row.email.trim(),
|
|
46
|
+
// Peer view rows carry only id/email/status; provider/framework default for a re-mint
|
|
47
|
+
// (the target machine logs in with its own framework — claude-code/anthropic by default).
|
|
48
|
+
provider: defaultProvider,
|
|
49
|
+
framework: defaultFramework,
|
|
50
|
+
label: accountId,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { resolved: false, reason: 'cannot resolve approved account email' };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=resolveFollowMeEnrollTarget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveFollowMeEnrollTarget.js","sourceRoot":"","sources":["../../src/core/resolveFollowMeEnrollTarget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoCH;;;;;;;GAOG;AACH,MAAM,UAAU,2BAA2B,CACzC,KAAuC;IAEvC,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAC5B,MAAM,eAAe,GAAG,KAAK,CAAC,eAAe,IAAI,WAAW,CAAC;IAC7D,MAAM,gBAAgB,GAAG,KAAK,CAAC,gBAAgB,IAAI,aAAa,CAAC;IAEjE,mDAAmD;IACnD,MAAM,KAAK,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC;IAClE,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9E,OAAO;YACL,QAAQ,EAAE,IAAI;YACd,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE;YACjC,QAAQ,EAAE,CAAC,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe;YAC1F,SAAS,EAAE,CAAC,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB;YAC/F,KAAK,EAAE,CAAC,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;SAChG,CAAC;IACJ,CAAC;IAED,uEAAuE;IACvE,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACnC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChC,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS;gBAAE,SAAS;YAC1C,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACjE,OAAO;oBACL,QAAQ,EAAE,IAAI;oBACd,aAAa,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE;oBAC/B,sFAAsF;oBACtF,0FAA0F;oBAC1F,QAAQ,EAAE,eAAe;oBACzB,SAAS,EAAE,gBAAgB;oBAC3B,KAAK,EAAE,SAAS;iBACjB,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,uCAAuC,EAAE,CAAC;AAC9E,CAAC"}
|