@camstack/core 0.1.37 → 0.1.39
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/auth/auth-manager.d.ts +12 -1
- package/dist/auth/auth-manager.d.ts.map +1 -1
- package/dist/auth/scope-matcher.d.ts +8 -0
- package/dist/auth/scope-matcher.d.ts.map +1 -0
- package/dist/auth/totp-manager.d.ts +0 -1
- package/dist/auth/totp-manager.d.ts.map +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +15 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts.map +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +27 -6
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +27 -6
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -1
- package/dist/builtins/device-manager/device-config-contribution.d.ts +33 -0
- package/dist/builtins/device-manager/device-config-contribution.d.ts.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.d.ts +52 -17
- package/dist/builtins/device-manager/device-manager.addon.d.ts.map +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +285 -161
- package/dist/builtins/device-manager/device-manager.addon.js.map +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +286 -162
- package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -1
- package/dist/builtins/local-auth/auth-schema.d.ts +1 -0
- package/dist/builtins/local-auth/auth-schema.d.ts.map +1 -1
- package/dist/builtins/local-auth/local-auth.addon.d.ts +1 -0
- package/dist/builtins/local-auth/local-auth.addon.d.ts.map +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +354 -3
- package/dist/builtins/local-auth/local-auth.addon.js.map +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +355 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -1
- package/dist/builtins/local-auth/oauth-grants.d.ts +46 -0
- package/dist/builtins/local-auth/oauth-grants.d.ts.map +1 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +51 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts.map +1 -0
- package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts +97 -0
- package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts.map +1 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +24 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts.map +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +136 -56
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js.map +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +137 -57
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs.map +1 -1
- package/dist/builtins/snapshot/index.js +1 -3
- package/dist/builtins/snapshot/index.js.map +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -3
- package/dist/builtins/snapshot/index.mjs.map +1 -1
- package/dist/builtins/snapshot/snapshot.addon.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +428 -234
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +428 -235
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -37
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts +0 -8
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts.map +0 -1
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js +0 -75
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js.map +0 -1
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs +0 -69
- package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs.map +0 -1
- package/dist/builtins/auth-orchestrator/index.d.ts +0 -2
- package/dist/builtins/auth-orchestrator/index.d.ts.map +0 -1
- package/dist/builtins/auth-orchestrator/index.js +0 -7
- package/dist/builtins/auth-orchestrator/index.mjs +0 -2
- package/dist/builtins/mesh-orchestrator/index.d.ts +0 -2
- package/dist/builtins/mesh-orchestrator/index.d.ts.map +0 -1
- package/dist/builtins/mesh-orchestrator/index.js +0 -7
- package/dist/builtins/mesh-orchestrator/index.mjs +0 -2
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts +0 -9
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts.map +0 -1
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js +0 -113
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js.map +0 -1
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs +0 -107
- package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs.map +0 -1
- package/dist/builtins/turn-orchestrator/index.d.ts +0 -2
- package/dist/builtins/turn-orchestrator/index.d.ts.map +0 -1
- package/dist/builtins/turn-orchestrator/index.js +0 -7
- package/dist/builtins/turn-orchestrator/index.mjs +0 -2
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts +0 -34
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts.map +0 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js +0 -126
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js.map +0 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs +0 -120
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs.map +0 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ISsoBridgeProvider, TokenScope } from '@camstack/types';
|
|
2
|
+
import { OauthSessionManager } from './oauth-session-manager.js';
|
|
3
|
+
interface IssueCodeInput {
|
|
4
|
+
integrationId: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
username: string;
|
|
7
|
+
scopes: TokenScope[];
|
|
8
|
+
redirectUri: string;
|
|
9
|
+
hubUrl: string;
|
|
10
|
+
}
|
|
11
|
+
interface TokenPair {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
expiresIn: number;
|
|
15
|
+
}
|
|
16
|
+
interface VerifiedAccess {
|
|
17
|
+
userId: string;
|
|
18
|
+
username: string;
|
|
19
|
+
scopes: TokenScope[];
|
|
20
|
+
}
|
|
21
|
+
/** OAuth account-linking grant logic. Pure over an injected sso-bridge
|
|
22
|
+
* signer so it is unit-testable without the capability registry.
|
|
23
|
+
*
|
|
24
|
+
* Authorization codes are short-lived (60 s). The `redirectUri`,
|
|
25
|
+
* `integrationId`, and a unique `jti` are embedded in the signed JWT
|
|
26
|
+
* so the HMAC signature is the real security boundary. Single-use is
|
|
27
|
+
* enforced by a `jti` consumed-set. The old in-process `pendingCodes`
|
|
28
|
+
* Map is gone — a hub restart no longer breaks in-flight exchanges,
|
|
29
|
+
* only risking a single replay within the remaining 60 s TTL. */
|
|
30
|
+
export declare function createOauthGrants(ssoBridge: ISsoBridgeProvider, sessionManager: OauthSessionManager): {
|
|
31
|
+
oauthIssueCode(input: IssueCodeInput): Promise<{
|
|
32
|
+
code: string;
|
|
33
|
+
}>;
|
|
34
|
+
oauthExchangeCode(input: {
|
|
35
|
+
code: string;
|
|
36
|
+
redirectUri: string;
|
|
37
|
+
}): Promise<TokenPair | null>;
|
|
38
|
+
oauthRefresh(input: {
|
|
39
|
+
refreshToken: string;
|
|
40
|
+
}): Promise<TokenPair | null>;
|
|
41
|
+
oauthVerifyAccessToken(input: {
|
|
42
|
+
token: string;
|
|
43
|
+
}): Promise<VerifiedAccess | null>;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=oauth-grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-grants.d.ts","sourceRoot":"","sources":["../../../src/builtins/local-auth/oauth-grants.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAIrE,UAAU,cAAc;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,SAAS;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,UAAU,EAAE,CAAA;CACrB;AAED;;;;;;;;kEAQkE;AAClE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,kBAAkB,EAAE,cAAc,EAAE,mBAAmB;0BAgCpE,cAAc,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;6BAevC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;wBA0BtE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;kCAe1C;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;EAiBzF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { TokenScope, SettingsStoreClient } from '@camstack/types';
|
|
2
|
+
export interface OauthSession {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly userId: string;
|
|
5
|
+
readonly username: string;
|
|
6
|
+
readonly integrationId: string;
|
|
7
|
+
readonly scopes: TokenScope[];
|
|
8
|
+
readonly createdAt: number;
|
|
9
|
+
readonly lastUsedAt: number;
|
|
10
|
+
readonly revokedAt: number | null;
|
|
11
|
+
}
|
|
12
|
+
interface CreateInput {
|
|
13
|
+
readonly userId: string;
|
|
14
|
+
readonly username: string;
|
|
15
|
+
readonly integrationId: string;
|
|
16
|
+
readonly scopes: TokenScope[];
|
|
17
|
+
}
|
|
18
|
+
export declare class OauthSessionManager {
|
|
19
|
+
private readonly store;
|
|
20
|
+
constructor(store: SettingsStoreClient);
|
|
21
|
+
/**
|
|
22
|
+
* Create a new OAuth session record. Generates a UUID as the session id,
|
|
23
|
+
* sets `createdAt` and `lastUsedAt` to now, `revokedAt` to null.
|
|
24
|
+
*/
|
|
25
|
+
create(input: CreateInput): Promise<OauthSession>;
|
|
26
|
+
/**
|
|
27
|
+
* Return all sessions — both active and revoked. Callers decide
|
|
28
|
+
* whether to filter by `revokedAt`.
|
|
29
|
+
*/
|
|
30
|
+
list(): Promise<OauthSession[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Look up a session by its id. Returns null when not found.
|
|
33
|
+
*/
|
|
34
|
+
getById(id: string): Promise<OauthSession | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Mark a session as revoked by setting `revokedAt = now`.
|
|
37
|
+
*
|
|
38
|
+
* - Returns `true` on success (including idempotent re-revoke — the
|
|
39
|
+
* existing `revokedAt` timestamp is preserved; it is NOT updated).
|
|
40
|
+
* - Returns `false` when the session id is not found.
|
|
41
|
+
*/
|
|
42
|
+
markRevoked(id: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Update `lastUsedAt` to now. No-op (does not throw) when the session
|
|
45
|
+
* id is not found — the caller (token-use hot path) should not fail
|
|
46
|
+
* for a missing session that may have been concurrently revoked.
|
|
47
|
+
*/
|
|
48
|
+
touch(id: string): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=oauth-session-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-session-manager.d.ts","sourceRoot":"","sources":["../../../src/builtins/local-auth/oauth-session-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAgBtE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAa,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAS,MAAM,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAO,MAAM,CAAA;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAS,UAAU,EAAE,CAAA;IACpC,QAAQ,CAAC,SAAS,EAAM,MAAM,CAAA;IAC9B,QAAQ,CAAC,UAAU,EAAK,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAM,MAAM,GAAG,IAAI,CAAA;CACtC;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,MAAM,EAAS,MAAM,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAO,MAAM,CAAA;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAS,UAAU,EAAE,CAAA;CACrC;AAgCD,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,mBAAmB;IAEvD;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAqBvD;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAQrC;;OAEG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IASvD;;;;;;OAMG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAe/C;;;;OAIG;IACG,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWvC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reconciliation core for the remote-access orchestrator's durable
|
|
3
|
+
* `enabledProviders` set (D8 delivery-rule fix).
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists
|
|
6
|
+
* ----------------------
|
|
7
|
+
* `enabledProviders: string[]` is the operator's "auto-start on boot"
|
|
8
|
+
* intent — a DURABLE setting. Historically it was written ONLY from two
|
|
9
|
+
* fire-and-forget bus events (`NetworkTunnelStarted` / `Stopped`). A
|
|
10
|
+
* dropped event (hub restart, addon crash-respawn, hub↔orchestrator
|
|
11
|
+
* partition) silently diverges the persisted set from reality:
|
|
12
|
+
* - dropped `Started` → tunnel running, not recorded → no auto-start
|
|
13
|
+
* - dropped `Stopped` → tunnel stopped, still recorded → boot restarts
|
|
14
|
+
* an operator-stopped tunnel.
|
|
15
|
+
*
|
|
16
|
+
* A durable setting must not be written by a lossy event. The fix is an
|
|
17
|
+
* idempotent reconcile pass that pulls authoritative state via an
|
|
18
|
+
* ACKNOWLEDGED RPC (`network-access` cap `getStatus`) on every provider
|
|
19
|
+
* (re)connect — mirroring the kernel readiness-registry hydration via
|
|
20
|
+
* `$readiness.getSnapshot`. The lifecycle events stay (still fine for
|
|
21
|
+
* live UI dashboards); only the PERSISTENCE WRITE moves to the RPC pull.
|
|
22
|
+
*
|
|
23
|
+
* Reconcile semantics — ADD-ONLY (deliberately conservative)
|
|
24
|
+
* ----------------------------------------------------------
|
|
25
|
+
* `connected: true` from an acked RPC is UNAMBIGUOUS: the tunnel is up,
|
|
26
|
+
* so the operator must have started it → safe to ADD to `enabledProviders`
|
|
27
|
+
* (recovers a dropped `NetworkTunnelStarted` event).
|
|
28
|
+
*
|
|
29
|
+
* `connected: false` is AMBIGUOUS:
|
|
30
|
+
* - Provider registered but not yet started (normal cold-boot state,
|
|
31
|
+
* BEFORE `autoStartEnabledProviders` has called `start()`).
|
|
32
|
+
* - Provider transiently down (network blip, restart in progress).
|
|
33
|
+
* - Provider intentionally stopped by the operator.
|
|
34
|
+
* `getStatus()` reports connectivity, NOT operator intent. Acting on
|
|
35
|
+
* `connected: false` would wipe `enabledProviders` on every hub restart
|
|
36
|
+
* (providers re-register ~15-20 s before `start()` is called), defeating
|
|
37
|
+
* the purpose of the durable enabled-set. Therefore the reconcile pass
|
|
38
|
+
* NEVER removes on `connected: false`.
|
|
39
|
+
*
|
|
40
|
+
* Removal of operator intent stays event-driven (`NetworkTunnelStopped`
|
|
41
|
+
* → `markEnabled(id, false)`). That event is emitted synchronously by
|
|
42
|
+
* the provider's own `stop()` call — it is the reliable signal that the
|
|
43
|
+
* operator actually stopped the tunnel. A failed/absent RPC probe also
|
|
44
|
+
* leaves the provider untouched.
|
|
45
|
+
*
|
|
46
|
+
* // Follow-up: covering the dropped-`Stopped` direction (a `Stopped`
|
|
47
|
+
* // event lost → tunnel stopped but still in enabledProviders → boot
|
|
48
|
+
* // wrongly restarts it) would require a provider-side persisted
|
|
49
|
+
* // operator-intent flag that `getStatus()` surfaces. That is a larger
|
|
50
|
+
* // multi-addon change (the audit's "approach 2") and is OUT OF SCOPE
|
|
51
|
+
* // for this backstop. The current fix covers the higher-impact direction
|
|
52
|
+
* // (dropped `Started` → operator's running tunnel not auto-restarted).
|
|
53
|
+
*
|
|
54
|
+
* The function is pure: the addon owns the RPC fan-out + the persist
|
|
55
|
+
* call, so this core is unit-testable without a CapabilityRegistry.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Outcome of one provider's `getStatus()` RPC probe.
|
|
59
|
+
*
|
|
60
|
+
* `ok: true` → the RPC was acknowledged; `connected` is authoritative.
|
|
61
|
+
* `ok: false` → the RPC threw / timed out; state is unknown and the
|
|
62
|
+
* provider's membership must be left as-is.
|
|
63
|
+
*/
|
|
64
|
+
export type ProviderProbeResult = {
|
|
65
|
+
readonly addonId: string;
|
|
66
|
+
readonly ok: true;
|
|
67
|
+
readonly connected: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
readonly addonId: string;
|
|
70
|
+
readonly ok: false;
|
|
71
|
+
};
|
|
72
|
+
export interface ReconcileResult {
|
|
73
|
+
/** The corrected enabled-set, sorted for stable persistence. */
|
|
74
|
+
readonly nextEnabled: readonly string[];
|
|
75
|
+
/** `true` when `nextEnabled` differs from the input — caller persists only then. */
|
|
76
|
+
readonly changed: boolean;
|
|
77
|
+
/** addonIds added because an acked RPC reported them connected. */
|
|
78
|
+
readonly added: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* Always empty — reconcile is add-only. Removal of operator intent is
|
|
81
|
+
* event-driven (`NetworkTunnelStopped`). Kept in the type so callers
|
|
82
|
+
* that log `result.removed` continue to compile without changes.
|
|
83
|
+
*/
|
|
84
|
+
readonly removed: readonly string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reconcile the durable `enabledProviders` set against authoritative
|
|
88
|
+
* RPC probe results. Pure — no I/O. ADD-ONLY: only adds providers whose
|
|
89
|
+
* acked `getStatus()` reports `connected: true`. Never removes on
|
|
90
|
+
* `connected: false` — see module doc for the full rationale.
|
|
91
|
+
*
|
|
92
|
+
* @param currentEnabled - the persisted enabled-set before reconcile.
|
|
93
|
+
* @param probes - one entry per provider whose `getStatus()` RPC was
|
|
94
|
+
* attempted this pass. Providers absent from this list are untouched.
|
|
95
|
+
*/
|
|
96
|
+
export declare function reconcileEnabledProviders(currentEnabled: readonly string[], probes: readonly ProviderProbeResult[]): ReconcileResult;
|
|
97
|
+
//# sourceMappingURL=enabled-providers-reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enabled-providers-reconcile.d.ts","sourceRoot":"","sources":["../../../src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAC5E;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;CAAE,CAAA;AAEpD,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,MAAM,EAAE,SAAS,mBAAmB,EAAE,GACrC,eAAe,CA0BjB"}
|
|
@@ -9,10 +9,33 @@ interface RemoteAccessOrchestratorConfig {
|
|
|
9
9
|
export declare class RemoteAccessOrchestratorAddon extends BaseAddon<RemoteAccessOrchestratorConfig> {
|
|
10
10
|
constructor();
|
|
11
11
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Maintain the persisted `enabledProviders` set from a tunnel
|
|
14
|
+
* lifecycle event. `source.id` is `string | number`; `network-access`
|
|
15
|
+
* providers emit with `type: 'addon'` so it is always the addonId
|
|
16
|
+
* string. Non-string / non-addon sources are ignored defensively.
|
|
17
|
+
*/
|
|
18
|
+
private onTunnelLifecycle;
|
|
19
|
+
/**
|
|
20
|
+
* Probe every locally-visible `network-access` provider's
|
|
21
|
+
* `getStatus()` over RPC and reconcile the durable `enabledProviders`
|
|
22
|
+
* set against the result. This is the D8 backstop for dropped
|
|
23
|
+
* `NetworkTunnelStarted` events: if `connected: true` is acked, the
|
|
24
|
+
* provider is added to `enabledProviders`. ADD-ONLY — never removes on
|
|
25
|
+
* `connected: false` (see enabled-providers-reconcile.ts for rationale).
|
|
26
|
+
*
|
|
27
|
+
* `getStatus()` is an acknowledged cap RPC — a transport blip surfaces
|
|
28
|
+
* as a rejected promise (recorded as `ok: false`, membership left
|
|
29
|
+
* untouched), never as a silent lossy write. Runs on every provider
|
|
30
|
+
* (re)connect via the `watchCapability` hooks, mirroring the kernel
|
|
31
|
+
* readiness-registry hydration on `$node.connected`. Safe to call
|
|
32
|
+
* before `autoStartEnabledProviders`: add-only semantics mean it cannot
|
|
33
|
+
* wipe the enabled-set even when providers are registered-not-yet-started.
|
|
34
|
+
*/
|
|
35
|
+
private reconcileAndPersist;
|
|
12
36
|
private autoStartEnabledProviders;
|
|
13
37
|
private markEnabled;
|
|
14
38
|
private resolveImpl;
|
|
15
|
-
private listProviders;
|
|
16
39
|
}
|
|
17
40
|
export default RemoteAccessOrchestratorAddon;
|
|
18
41
|
//# sourceMappingURL=remote-access-orchestrator.addon.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-access-orchestrator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"remote-access-orchestrator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EACL,SAAS,EAET,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAA;AAexB,UAAU,8BAA8B;IACtC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7C;AAED,qBAAa,6BAA8B,SAAQ,SAAS,CAAC,8BAA8B,CAAC;;cAK1E,YAAY,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAwD/D;;;;;OAKG;YACW,iBAAiB;IAa/B;;;;;;;;;;;;;;;OAeG;YACW,mBAAmB;YAiCnB,yBAAyB;YAmDzB,WAAW;IAWzB,OAAO,CAAC,WAAW;CAOpB;AAED,eAAe,6BAA6B,CAAA"}
|
|
@@ -4,42 +4,81 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-C13QxCFV.js");
|
|
6
6
|
let _camstack_types = require("@camstack/types");
|
|
7
|
+
//#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
|
|
8
|
+
/**
|
|
9
|
+
* Reconcile the durable `enabledProviders` set against authoritative
|
|
10
|
+
* RPC probe results. Pure — no I/O. ADD-ONLY: only adds providers whose
|
|
11
|
+
* acked `getStatus()` reports `connected: true`. Never removes on
|
|
12
|
+
* `connected: false` — see module doc for the full rationale.
|
|
13
|
+
*
|
|
14
|
+
* @param currentEnabled - the persisted enabled-set before reconcile.
|
|
15
|
+
* @param probes - one entry per provider whose `getStatus()` RPC was
|
|
16
|
+
* attempted this pass. Providers absent from this list are untouched.
|
|
17
|
+
*/
|
|
18
|
+
function reconcileEnabledProviders(currentEnabled, probes) {
|
|
19
|
+
const next = new Set(currentEnabled);
|
|
20
|
+
const added = [];
|
|
21
|
+
for (const probe of probes) {
|
|
22
|
+
if (!probe.ok) continue;
|
|
23
|
+
if (probe.connected) {
|
|
24
|
+
if (!next.has(probe.addonId)) {
|
|
25
|
+
next.add(probe.addonId);
|
|
26
|
+
added.push(probe.addonId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
nextEnabled: [...next].sort(),
|
|
32
|
+
changed: added.length > 0,
|
|
33
|
+
added,
|
|
34
|
+
removed: []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
7
38
|
//#region src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts
|
|
8
39
|
/**
|
|
9
|
-
* Remote-access orchestrator —
|
|
10
|
-
* `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).
|
|
11
|
-
*
|
|
40
|
+
* Remote-access orchestrator — backend-only boot-autostart service for
|
|
41
|
+
* the `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).
|
|
42
|
+
*
|
|
43
|
+
* Retired its `remote-access` facade cap (2026-05-15): the admin UI now
|
|
44
|
+
* talks to the `network-access` collection cap directly via generic
|
|
45
|
+
* per-`addonId` routing, so this addon registers NO capability.
|
|
12
46
|
*
|
|
13
|
-
*
|
|
47
|
+
* What it still owns — the load-bearing logic:
|
|
14
48
|
* The orchestrator owns the "operator wants this provider running"
|
|
15
|
-
* intent —
|
|
49
|
+
* intent — an `enabledProviders: string[]` slice in its addon-store
|
|
16
50
|
* blob (BaseAddon.config). On boot we iterate the list and call
|
|
17
|
-
* `provider.start()` for each enabled entry
|
|
18
|
-
*
|
|
19
|
-
*
|
|
51
|
+
* `provider.start()` for each enabled entry, so a tunnel set up once
|
|
52
|
+
* stays up across hub restarts.
|
|
53
|
+
*
|
|
54
|
+
* Since start/stop no longer flow through this addon, the enabled-set
|
|
55
|
+
* is kept in sync from two sources:
|
|
56
|
+
* 1. `NetworkTunnelStarted` / `NetworkTunnelStopped` bus events —
|
|
57
|
+
* fast, but lossy (fire-and-forget broadcasts). They drive both
|
|
58
|
+
* the live UI and removal from the durable set (`Stopped` is the
|
|
59
|
+
* only reliable "operator stopped it" signal).
|
|
60
|
+
* 2. An RPC-driven ADD-ONLY RECONCILE pass (`reconcileEnabledProviders`)
|
|
61
|
+
* that pulls authoritative `connected` state via the `network-access`
|
|
62
|
+
* cap's `getStatus()` on every provider (re)connect. THIS covers
|
|
63
|
+
* dropped `NetworkTunnelStarted` events — if `connected: true` is
|
|
64
|
+
* acked, the provider is added to `enabledProviders`. It NEVER
|
|
65
|
+
* removes on `connected: false` because that signal is ambiguous
|
|
66
|
+
* (registered-not-yet-started vs transiently-down vs intentionally
|
|
67
|
+
* stopped). Running before `autoStartEnabledProviders` is safe:
|
|
68
|
+
* it can only add already-connected providers, never evict.
|
|
20
69
|
*/
|
|
21
70
|
var RemoteAccessOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
22
71
|
constructor() {
|
|
23
72
|
super({ enabledProviders: [] });
|
|
24
73
|
}
|
|
25
74
|
async onInitialize() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return endpoint;
|
|
34
|
-
},
|
|
35
|
-
stopProvider: async ({ addonId }) => {
|
|
36
|
-
const impl = this.resolveImpl(addonId);
|
|
37
|
-
if (impl?.stop) await impl.stop();
|
|
38
|
-
await this.markEnabled(addonId, false);
|
|
39
|
-
return { success: true };
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
this.ctx.logger.info("Remote-access orchestrator initialized", { meta: { enabledCount: this.config.enabledProviders.length } });
|
|
75
|
+
this.ctx.logger.info("Remote-access orchestrator initialized (backend-only)", { meta: { enabledCount: this.config.enabledProviders.length } });
|
|
76
|
+
this.ctx.eventBus?.subscribe({ category: _camstack_types.EventCategory.NetworkTunnelStarted }, (event) => {
|
|
77
|
+
this.onTunnelLifecycle(event.source, true);
|
|
78
|
+
});
|
|
79
|
+
this.ctx.eventBus?.subscribe({ category: _camstack_types.EventCategory.NetworkTunnelStopped }, (event) => {
|
|
80
|
+
this.onTunnelLifecycle(event.source, false);
|
|
81
|
+
});
|
|
43
82
|
setImmediate(() => {
|
|
44
83
|
this.autoStartEnabledProviders();
|
|
45
84
|
});
|
|
@@ -49,12 +88,76 @@ var RemoteAccessOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
|
49
88
|
this.watchCapability("mesh-network", { onReady: () => {
|
|
50
89
|
this.autoStartEnabledProviders();
|
|
51
90
|
} });
|
|
52
|
-
return [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Maintain the persisted `enabledProviders` set from a tunnel
|
|
95
|
+
* lifecycle event. `source.id` is `string | number`; `network-access`
|
|
96
|
+
* providers emit with `type: 'addon'` so it is always the addonId
|
|
97
|
+
* string. Non-string / non-addon sources are ignored defensively.
|
|
98
|
+
*/
|
|
99
|
+
async onTunnelLifecycle(source, started) {
|
|
100
|
+
if (source.type !== "addon" || typeof source.id !== "string") {
|
|
101
|
+
this.ctx.logger.warn("tunnel lifecycle event with non-addon source — ignoring", { meta: {
|
|
102
|
+
sourceType: source.type,
|
|
103
|
+
sourceId: source.id,
|
|
104
|
+
started
|
|
105
|
+
} });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await this.markEnabled(source.id, started);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Probe every locally-visible `network-access` provider's
|
|
112
|
+
* `getStatus()` over RPC and reconcile the durable `enabledProviders`
|
|
113
|
+
* set against the result. This is the D8 backstop for dropped
|
|
114
|
+
* `NetworkTunnelStarted` events: if `connected: true` is acked, the
|
|
115
|
+
* provider is added to `enabledProviders`. ADD-ONLY — never removes on
|
|
116
|
+
* `connected: false` (see enabled-providers-reconcile.ts for rationale).
|
|
117
|
+
*
|
|
118
|
+
* `getStatus()` is an acknowledged cap RPC — a transport blip surfaces
|
|
119
|
+
* as a rejected promise (recorded as `ok: false`, membership left
|
|
120
|
+
* untouched), never as a silent lossy write. Runs on every provider
|
|
121
|
+
* (re)connect via the `watchCapability` hooks, mirroring the kernel
|
|
122
|
+
* readiness-registry hydration on `$node.connected`. Safe to call
|
|
123
|
+
* before `autoStartEnabledProviders`: add-only semantics mean it cannot
|
|
124
|
+
* wipe the enabled-set even when providers are registered-not-yet-started.
|
|
125
|
+
*/
|
|
126
|
+
async reconcileAndPersist() {
|
|
127
|
+
const entries = this.capabilities?.getCollectionEntries("network-access") ?? [];
|
|
128
|
+
if (entries.length === 0) return;
|
|
129
|
+
const probes = await Promise.all(entries.map(async ([addonId, impl]) => {
|
|
130
|
+
if (!impl.getStatus) return {
|
|
131
|
+
addonId,
|
|
132
|
+
ok: false
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
return {
|
|
136
|
+
addonId,
|
|
137
|
+
ok: true,
|
|
138
|
+
connected: (await impl.getStatus()).connected
|
|
139
|
+
};
|
|
140
|
+
} catch (err) {
|
|
141
|
+
this.ctx.logger.warn("reconcile: getStatus RPC failed — leaving membership as-is", { meta: {
|
|
142
|
+
addonId,
|
|
143
|
+
error: err instanceof Error ? err.message : String(err)
|
|
144
|
+
} });
|
|
145
|
+
return {
|
|
146
|
+
addonId,
|
|
147
|
+
ok: false
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}));
|
|
151
|
+
const result = reconcileEnabledProviders(this.config.enabledProviders, probes);
|
|
152
|
+
if (!result.changed) return;
|
|
153
|
+
this.ctx.logger.info("reconcile: corrected enabledProviders from RPC-pulled state", { meta: {
|
|
154
|
+
added: result.added,
|
|
155
|
+
removed: result.removed
|
|
156
|
+
} });
|
|
157
|
+
await this.updateGlobalSettings({ enabledProviders: result.nextEnabled });
|
|
56
158
|
}
|
|
57
159
|
async autoStartEnabledProviders() {
|
|
160
|
+
await this.reconcileAndPersist();
|
|
58
161
|
const ids = this.config.enabledProviders;
|
|
59
162
|
if (ids.length === 0) return;
|
|
60
163
|
this.ctx.logger.info("Auto-starting enabled remote-access providers", { meta: { addonIds: [...ids] } });
|
|
@@ -96,38 +199,15 @@ var RemoteAccessOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
|
96
199
|
if (enabled) current.add(addonId);
|
|
97
200
|
else current.delete(addonId);
|
|
98
201
|
if (wasEnabled === enabled) return;
|
|
202
|
+
this.ctx.logger.info("remote-access intent updated", { meta: {
|
|
203
|
+
addonId,
|
|
204
|
+
enabled
|
|
205
|
+
} });
|
|
99
206
|
await this.updateGlobalSettings({ enabledProviders: [...current] });
|
|
100
207
|
}
|
|
101
208
|
resolveImpl(addonId) {
|
|
102
209
|
return (this.capabilities?.getCollectionEntries("network-access") ?? []).find(([id]) => id === addonId)?.[1] ?? null;
|
|
103
210
|
}
|
|
104
|
-
async listProviders() {
|
|
105
|
-
const entries = this.capabilities?.getCollectionEntries("network-access") ?? [];
|
|
106
|
-
const enabled = new Set(this.config.enabledProviders);
|
|
107
|
-
const out = [];
|
|
108
|
-
for (const [addonId, impl] of entries) {
|
|
109
|
-
let connected = false;
|
|
110
|
-
let endpoint = null;
|
|
111
|
-
let error;
|
|
112
|
-
if (impl.getStatus) try {
|
|
113
|
-
const s = await impl.getStatus();
|
|
114
|
-
connected = s.connected;
|
|
115
|
-
endpoint = s.endpoint;
|
|
116
|
-
error = s.error;
|
|
117
|
-
} catch (err) {
|
|
118
|
-
error = err instanceof Error ? err.message : String(err);
|
|
119
|
-
}
|
|
120
|
-
out.push({
|
|
121
|
-
addonId,
|
|
122
|
-
displayName: impl.displayName ?? addonId,
|
|
123
|
-
enabled: enabled.has(addonId),
|
|
124
|
-
connected,
|
|
125
|
-
endpoint,
|
|
126
|
-
...error !== void 0 ? { error } : {}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
211
|
};
|
|
132
212
|
//#endregion
|
|
133
213
|
exports.RemoteAccessOrchestratorAddon = RemoteAccessOrchestratorAddon;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-access-orchestrator.addon.js","names":[],"sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"sourcesContent":["/**\n * Remote-access orchestrator — singleton facade over the\n * `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).\n * Mirrors the auth-orchestrator and backup-orchestrator patterns.\n *\n * Persistence + autostart contract:\n * The orchestrator owns the \"operator wants this provider running\"\n * intent — a `enabledProviders: string[]` slice in its addon-store\n * blob (BaseAddon.config). On boot we iterate the list and call\n * `provider.start()` for each enabled entry. `startProvider` /\n * `stopProvider` mutate this list so a Start press persists across\n * restarts. Same shape as turn-orchestrator's setProviderEnabled.\n */\nimport {\n BaseAddon,\n remoteAccessCapability,\n type IRemoteAccessOrchestrator,\n type RemoteAccessProviderInfo,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface NetworkAccessLike {\n start?: () => Promise<{ url: string; hostname: string; port: number; protocol: 'http' | 'https' }>\n stop?: () => Promise<void>\n getStatus?: () => Promise<{\n connected: boolean\n endpoint: { url: string; hostname: string; port: number; protocol: 'http' | 'https' } | null\n error?: string\n }>\n}\n\ninterface NetworkAccessRegistrationMeta {\n readonly displayName?: string\n}\n\ninterface RemoteAccessOrchestratorConfig {\n /**\n * addonIds the operator has explicitly Started. Auto-respawned on\n * boot so a tunnel set up once stays up across hub restarts.\n */\n readonly enabledProviders: readonly string[]\n}\n\nexport class RemoteAccessOrchestratorAddon extends BaseAddon<RemoteAccessOrchestratorConfig> {\n constructor() {\n super({ enabledProviders: [] })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n const provider: IRemoteAccessOrchestrator = {\n listProviders: async () => this.listProviders(),\n startProvider: async ({ addonId }) => {\n const impl = this.resolveImpl(addonId)\n if (!impl?.start) throw new Error(`Remote-access provider \"${addonId}\" does not support start`)\n const endpoint = await impl.start()\n // Persist intent — next boot will auto-respawn this provider.\n await this.markEnabled(addonId, true)\n return endpoint\n },\n stopProvider: async ({ addonId }) => {\n const impl = this.resolveImpl(addonId)\n if (impl?.stop) await impl.stop()\n // Clear intent — boot must NOT respawn this on next start.\n await this.markEnabled(addonId, false)\n return { success: true as const }\n },\n } satisfies IRemoteAccessOrchestrator\n this.ctx.logger.info('Remote-access orchestrator initialized', {\n meta: { enabledCount: this.config.enabledProviders.length },\n })\n // Defer autostart to next tick so the orchestrator's own provider\n // registration completes first. `resolveImpl` reads from the\n // capabilities registry which only sees in-process / cluster-mirrored\n // providers once they've ALSO registered — small delay gives the\n // cluster bridge time to discover them on cold boot. Errors are\n // logged but never block init.\n setImmediate(() => { this.autoStartEnabledProviders() })\n\n // Lazy retry — forked providers (cloudflare-tunnel etc) typically\n // register 15-20 s after this orchestrator boots, well past the\n // `setImmediate` above. Hook BaseAddon's `system.ready-state`\n // subscription so we re-run autoStart every time the\n // `network-access` cap transitions to ready (whichever node holds\n // it). The inner logic is idempotent + skips already-connected\n // providers.\n this.watchCapability('network-access', {\n onReady: () => { void this.autoStartEnabledProviders() },\n })\n\n // Same watch for `mesh-network`. The tailscale-ingress provider\n // registers `network-access` synchronously at boot, but `start()`\n // throws when the tailnet isn't joined yet — so the boot-time\n // autoStart call fails for tailscale ingresses if the tailscale\n // daemon hadn't logged in by then. Watching `mesh-network` here\n // re-triggers autoStart the moment the client transitions to\n // joined (manual operator login or auto-rejoin), without needing\n // a server restart. Same idempotency rules: providers already\n // connected are skipped.\n this.watchCapability('mesh-network', {\n onReady: () => { void this.autoStartEnabledProviders() },\n })\n\n return [{ capability: remoteAccessCapability, provider }]\n }\n\n private async autoStartEnabledProviders(): Promise<void> {\n const ids = this.config.enabledProviders\n if (ids.length === 0) return\n this.ctx.logger.info('Auto-starting enabled remote-access providers', {\n meta: { addonIds: [...ids] },\n })\n for (const addonId of ids) {\n try {\n const impl = this.resolveImpl(addonId)\n if (!impl?.start) {\n // Provider isn't loaded yet (worker bridge still hydrating)\n // OR it doesn't implement start. Log at debug level — the\n // provider-registered subscription below will retry as soon\n // as the addon appears.\n this.ctx.logger.warn('autostart: provider not ready or unsupported', {\n meta: { addonId, hasImpl: !!impl, hasStart: !!impl?.start },\n })\n continue\n }\n // Idempotent: skip when the provider is already connected.\n // Avoids spamming start() on every provider-registered event\n // and prevents respawning a child process that's already alive.\n if (impl.getStatus) {\n const status = await impl.getStatus().catch(() => null)\n if (status?.connected) {\n this.ctx.logger.info('autostart: provider already connected — skipping', {\n meta: { addonId, url: status.endpoint?.url },\n })\n continue\n }\n }\n const endpoint = await impl.start()\n this.ctx.logger.info('autostart: provider started', {\n meta: { addonId, url: endpoint.url },\n })\n } catch (err) {\n this.ctx.logger.error('autostart: provider start failed', {\n meta: {\n addonId,\n error: err instanceof Error ? err.message : String(err),\n },\n })\n }\n }\n }\n\n private async markEnabled(addonId: string, enabled: boolean): Promise<void> {\n const current = new Set(this.config.enabledProviders)\n const wasEnabled = current.has(addonId)\n if (enabled) current.add(addonId); else current.delete(addonId)\n if (wasEnabled === enabled) return\n await this.updateGlobalSettings({ enabledProviders: [...current] })\n }\n\n private resolveImpl(addonId: string): (NetworkAccessLike & NetworkAccessRegistrationMeta) | null {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike & NetworkAccessRegistrationMeta>(\n 'network-access',\n ) ?? []\n const found = entries.find(([id]) => id === addonId)\n return found?.[1] ?? null\n }\n\n private async listProviders(): Promise<readonly RemoteAccessProviderInfo[]> {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike & NetworkAccessRegistrationMeta>(\n 'network-access',\n ) ?? []\n const enabled = new Set(this.config.enabledProviders)\n const out: RemoteAccessProviderInfo[] = []\n for (const [addonId, impl] of entries) {\n let connected = false\n let endpoint: RemoteAccessProviderInfo['endpoint'] = null\n let error: string | undefined\n if (impl.getStatus) {\n try {\n const s = await impl.getStatus()\n connected = s.connected\n endpoint = s.endpoint\n error = s.error\n } catch (err) {\n error = err instanceof Error ? err.message : String(err)\n }\n }\n out.push({\n addonId,\n displayName: impl.displayName ?? addonId,\n // `enabled` is now the operator's persisted intent — orthogonal\n // to `connected` (which reflects the live tunnel state). Boot\n // tries to bring enabled→connected automatically.\n enabled: enabled.has(addonId),\n connected,\n endpoint,\n ...(error !== undefined ? { error } : {}),\n })\n }\n return out\n }\n}\n\nexport default RemoteAccessOrchestratorAddon\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"remote-access-orchestrator.addon.js","names":[],"sources":["../../../src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts","../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"sourcesContent":["/**\n * Pure reconciliation core for the remote-access orchestrator's durable\n * `enabledProviders` set (D8 delivery-rule fix).\n *\n * Why this module exists\n * ----------------------\n * `enabledProviders: string[]` is the operator's \"auto-start on boot\"\n * intent — a DURABLE setting. Historically it was written ONLY from two\n * fire-and-forget bus events (`NetworkTunnelStarted` / `Stopped`). A\n * dropped event (hub restart, addon crash-respawn, hub↔orchestrator\n * partition) silently diverges the persisted set from reality:\n * - dropped `Started` → tunnel running, not recorded → no auto-start\n * - dropped `Stopped` → tunnel stopped, still recorded → boot restarts\n * an operator-stopped tunnel.\n *\n * A durable setting must not be written by a lossy event. The fix is an\n * idempotent reconcile pass that pulls authoritative state via an\n * ACKNOWLEDGED RPC (`network-access` cap `getStatus`) on every provider\n * (re)connect — mirroring the kernel readiness-registry hydration via\n * `$readiness.getSnapshot`. The lifecycle events stay (still fine for\n * live UI dashboards); only the PERSISTENCE WRITE moves to the RPC pull.\n *\n * Reconcile semantics — ADD-ONLY (deliberately conservative)\n * ----------------------------------------------------------\n * `connected: true` from an acked RPC is UNAMBIGUOUS: the tunnel is up,\n * so the operator must have started it → safe to ADD to `enabledProviders`\n * (recovers a dropped `NetworkTunnelStarted` event).\n *\n * `connected: false` is AMBIGUOUS:\n * - Provider registered but not yet started (normal cold-boot state,\n * BEFORE `autoStartEnabledProviders` has called `start()`).\n * - Provider transiently down (network blip, restart in progress).\n * - Provider intentionally stopped by the operator.\n * `getStatus()` reports connectivity, NOT operator intent. Acting on\n * `connected: false` would wipe `enabledProviders` on every hub restart\n * (providers re-register ~15-20 s before `start()` is called), defeating\n * the purpose of the durable enabled-set. Therefore the reconcile pass\n * NEVER removes on `connected: false`.\n *\n * Removal of operator intent stays event-driven (`NetworkTunnelStopped`\n * → `markEnabled(id, false)`). That event is emitted synchronously by\n * the provider's own `stop()` call — it is the reliable signal that the\n * operator actually stopped the tunnel. A failed/absent RPC probe also\n * leaves the provider untouched.\n *\n * // Follow-up: covering the dropped-`Stopped` direction (a `Stopped`\n * // event lost → tunnel stopped but still in enabledProviders → boot\n * // wrongly restarts it) would require a provider-side persisted\n * // operator-intent flag that `getStatus()` surfaces. That is a larger\n * // multi-addon change (the audit's \"approach 2\") and is OUT OF SCOPE\n * // for this backstop. The current fix covers the higher-impact direction\n * // (dropped `Started` → operator's running tunnel not auto-restarted).\n *\n * The function is pure: the addon owns the RPC fan-out + the persist\n * call, so this core is unit-testable without a CapabilityRegistry.\n */\n\n/**\n * Outcome of one provider's `getStatus()` RPC probe.\n *\n * `ok: true` → the RPC was acknowledged; `connected` is authoritative.\n * `ok: false` → the RPC threw / timed out; state is unknown and the\n * provider's membership must be left as-is.\n */\nexport type ProviderProbeResult =\n | { readonly addonId: string; readonly ok: true; readonly connected: boolean }\n | { readonly addonId: string; readonly ok: false }\n\nexport interface ReconcileResult {\n /** The corrected enabled-set, sorted for stable persistence. */\n readonly nextEnabled: readonly string[]\n /** `true` when `nextEnabled` differs from the input — caller persists only then. */\n readonly changed: boolean\n /** addonIds added because an acked RPC reported them connected. */\n readonly added: readonly string[]\n /**\n * Always empty — reconcile is add-only. Removal of operator intent is\n * event-driven (`NetworkTunnelStopped`). Kept in the type so callers\n * that log `result.removed` continue to compile without changes.\n */\n readonly removed: readonly string[]\n}\n\n/**\n * Reconcile the durable `enabledProviders` set against authoritative\n * RPC probe results. Pure — no I/O. ADD-ONLY: only adds providers whose\n * acked `getStatus()` reports `connected: true`. Never removes on\n * `connected: false` — see module doc for the full rationale.\n *\n * @param currentEnabled - the persisted enabled-set before reconcile.\n * @param probes - one entry per provider whose `getStatus()` RPC was\n * attempted this pass. Providers absent from this list are untouched.\n */\nexport function reconcileEnabledProviders(\n currentEnabled: readonly string[],\n probes: readonly ProviderProbeResult[],\n): ReconcileResult {\n const next = new Set<string>(currentEnabled)\n const added: string[] = []\n\n for (const probe of probes) {\n if (!probe.ok) continue // RPC failed → state unknown → leave membership as-is\n if (probe.connected) {\n // Acked connected:true is unambiguous → operator must have started\n // this tunnel → ensure it is in the enabled-set (recovers a dropped\n // NetworkTunnelStarted event).\n if (!next.has(probe.addonId)) {\n next.add(probe.addonId)\n added.push(probe.addonId)\n }\n }\n // connected:false → ambiguous (not-yet-started / transient blip /\n // operator-stopped). Do NOT remove. Removal is event-driven only.\n }\n\n const nextEnabled = [...next].sort()\n return {\n nextEnabled,\n changed: added.length > 0,\n added,\n removed: [], // always empty — reconcile is add-only\n }\n}\n","/**\n * Remote-access orchestrator — backend-only boot-autostart service for\n * the `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).\n *\n * Retired its `remote-access` facade cap (2026-05-15): the admin UI now\n * talks to the `network-access` collection cap directly via generic\n * per-`addonId` routing, so this addon registers NO capability.\n *\n * What it still owns — the load-bearing logic:\n * The orchestrator owns the \"operator wants this provider running\"\n * intent — an `enabledProviders: string[]` slice in its addon-store\n * blob (BaseAddon.config). On boot we iterate the list and call\n * `provider.start()` for each enabled entry, so a tunnel set up once\n * stays up across hub restarts.\n *\n * Since start/stop no longer flow through this addon, the enabled-set\n * is kept in sync from two sources:\n * 1. `NetworkTunnelStarted` / `NetworkTunnelStopped` bus events —\n * fast, but lossy (fire-and-forget broadcasts). They drive both\n * the live UI and removal from the durable set (`Stopped` is the\n * only reliable \"operator stopped it\" signal).\n * 2. An RPC-driven ADD-ONLY RECONCILE pass (`reconcileEnabledProviders`)\n * that pulls authoritative `connected` state via the `network-access`\n * cap's `getStatus()` on every provider (re)connect. THIS covers\n * dropped `NetworkTunnelStarted` events — if `connected: true` is\n * acked, the provider is added to `enabledProviders`. It NEVER\n * removes on `connected: false` because that signal is ambiguous\n * (registered-not-yet-started vs transiently-down vs intentionally\n * stopped). Running before `autoStartEnabledProviders` is safe:\n * it can only add already-connected providers, never evict.\n */\nimport {\n BaseAddon,\n EventCategory,\n type ProviderRegistration,\n} from '@camstack/types'\nimport {\n reconcileEnabledProviders,\n type ProviderProbeResult,\n} from './enabled-providers-reconcile.js'\n\ninterface NetworkAccessLike {\n start?: () => Promise<{ url: string; hostname: string; port: number; protocol: 'http' | 'https' }>\n getStatus?: () => Promise<{\n connected: boolean\n endpoint: { url: string; hostname: string; port: number; protocol: 'http' | 'https' } | null\n error?: string\n }>\n}\n\ninterface RemoteAccessOrchestratorConfig {\n /**\n * addonIds the operator has explicitly Started. Auto-respawned on\n * boot so a tunnel set up once stays up across hub restarts.\n */\n readonly enabledProviders: readonly string[]\n}\n\nexport class RemoteAccessOrchestratorAddon extends BaseAddon<RemoteAccessOrchestratorConfig> {\n constructor() {\n super({ enabledProviders: [] })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('Remote-access orchestrator initialized (backend-only)', {\n meta: { enabledCount: this.config.enabledProviders.length },\n })\n\n // Lifecycle events give the persisted set a PROMPT update — but\n // they are lossy fire-and-forget broadcasts, so they are NOT the\n // authority. The RPC-driven `reconcileAndPersist` pass below is the\n // backstop that corrects any divergence from a dropped event. The\n // emitting addonId is carried on `event.source.id` (events are\n // emitted with `source: { type: 'addon', id: ctx.id }`).\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStarted },\n (event) => { void this.onTunnelLifecycle(event.source, true) },\n )\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStopped },\n (event) => { void this.onTunnelLifecycle(event.source, false) },\n )\n\n // Defer autostart to next tick so provider registrations from\n // co-located addons settle first. `resolveImpl` reads from the\n // capabilities registry which only sees in-process / cluster-mirrored\n // providers once they've ALSO registered — small delay gives the\n // cluster bridge time to discover them on cold boot. Errors are\n // logged but never block init.\n setImmediate(() => { void this.autoStartEnabledProviders() })\n\n // Lazy retry — forked providers (cloudflare-tunnel etc) typically\n // register 15-20 s after this orchestrator boots, well past the\n // `setImmediate` above. Hook BaseAddon's `system.ready-state`\n // subscription so we re-run autoStart every time the\n // `network-access` cap transitions to ready (whichever node holds\n // it). The inner logic is idempotent + skips already-connected\n // providers.\n this.watchCapability('network-access', {\n onReady: () => { void this.autoStartEnabledProviders() },\n })\n\n // Same watch for `mesh-network`. The tailscale-ingress provider\n // registers `network-access` synchronously at boot, but `start()`\n // throws when the tailnet isn't joined yet — so the boot-time\n // autoStart call fails for tailscale ingresses if the tailscale\n // daemon hadn't logged in by then. Watching `mesh-network` here\n // re-triggers autoStart the moment the client transitions to\n // joined (manual operator login or auto-rejoin), without needing\n // a server restart. Same idempotency rules: providers already\n // connected are skipped.\n this.watchCapability('mesh-network', {\n onReady: () => { void this.autoStartEnabledProviders() },\n })\n\n // Backend-only addon — registers no capability.\n return []\n }\n\n /**\n * Maintain the persisted `enabledProviders` set from a tunnel\n * lifecycle event. `source.id` is `string | number`; `network-access`\n * providers emit with `type: 'addon'` so it is always the addonId\n * string. Non-string / non-addon sources are ignored defensively.\n */\n private async onTunnelLifecycle(\n source: { readonly type: string; readonly id: string | number },\n started: boolean,\n ): Promise<void> {\n if (source.type !== 'addon' || typeof source.id !== 'string') {\n this.ctx.logger.warn('tunnel lifecycle event with non-addon source — ignoring', {\n meta: { sourceType: source.type, sourceId: source.id, started },\n })\n return\n }\n await this.markEnabled(source.id, started)\n }\n\n /**\n * Probe every locally-visible `network-access` provider's\n * `getStatus()` over RPC and reconcile the durable `enabledProviders`\n * set against the result. This is the D8 backstop for dropped\n * `NetworkTunnelStarted` events: if `connected: true` is acked, the\n * provider is added to `enabledProviders`. ADD-ONLY — never removes on\n * `connected: false` (see enabled-providers-reconcile.ts for rationale).\n *\n * `getStatus()` is an acknowledged cap RPC — a transport blip surfaces\n * as a rejected promise (recorded as `ok: false`, membership left\n * untouched), never as a silent lossy write. Runs on every provider\n * (re)connect via the `watchCapability` hooks, mirroring the kernel\n * readiness-registry hydration on `$node.connected`. Safe to call\n * before `autoStartEnabledProviders`: add-only semantics mean it cannot\n * wipe the enabled-set even when providers are registered-not-yet-started.\n */\n private async reconcileAndPersist(): Promise<void> {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike>(\n 'network-access',\n ) ?? []\n if (entries.length === 0) return\n\n const probes: ProviderProbeResult[] = await Promise.all(\n entries.map(async ([addonId, impl]): Promise<ProviderProbeResult> => {\n if (!impl.getStatus) {\n // Provider can't report status — treat as unknown, leave as-is.\n return { addonId, ok: false }\n }\n try {\n const status = await impl.getStatus()\n return { addonId, ok: true, connected: status.connected }\n } catch (err) {\n this.ctx.logger.warn('reconcile: getStatus RPC failed — leaving membership as-is', {\n meta: { addonId, error: err instanceof Error ? err.message : String(err) },\n })\n return { addonId, ok: false }\n }\n }),\n )\n\n const result = reconcileEnabledProviders(this.config.enabledProviders, probes)\n if (!result.changed) return\n\n this.ctx.logger.info('reconcile: corrected enabledProviders from RPC-pulled state', {\n meta: { added: result.added, removed: result.removed },\n })\n await this.updateGlobalSettings({ enabledProviders: result.nextEnabled })\n }\n\n private async autoStartEnabledProviders(): Promise<void> {\n // RPC-driven ADD-ONLY reconcile first: recovers any dropped\n // `NetworkTunnelStarted` events by adding providers that are already\n // connected. Cannot evict — safe to run before start() calls.\n await this.reconcileAndPersist()\n\n const ids = this.config.enabledProviders\n if (ids.length === 0) return\n this.ctx.logger.info('Auto-starting enabled remote-access providers', {\n meta: { addonIds: [...ids] },\n })\n for (const addonId of ids) {\n try {\n const impl = this.resolveImpl(addonId)\n if (!impl?.start) {\n // Provider isn't loaded yet (worker bridge still hydrating)\n // OR it doesn't implement start. Log at warn level — the\n // `watchCapability` subscriptions above retry as soon as the\n // provider appears.\n this.ctx.logger.warn('autostart: provider not ready or unsupported', {\n meta: { addonId, hasImpl: !!impl, hasStart: !!impl?.start },\n })\n continue\n }\n // Idempotent: skip when the provider is already connected.\n // Avoids spamming start() on every ready-state event and\n // prevents respawning a child process that's already alive.\n if (impl.getStatus) {\n const status = await impl.getStatus().catch(() => null)\n if (status?.connected) {\n this.ctx.logger.info('autostart: provider already connected — skipping', {\n meta: { addonId, url: status.endpoint?.url },\n })\n continue\n }\n }\n const endpoint = await impl.start()\n this.ctx.logger.info('autostart: provider started', {\n meta: { addonId, url: endpoint.url },\n })\n } catch (err) {\n this.ctx.logger.error('autostart: provider start failed', {\n meta: {\n addonId,\n error: err instanceof Error ? err.message : String(err),\n },\n })\n }\n }\n }\n\n private async markEnabled(addonId: string, enabled: boolean): Promise<void> {\n const current = new Set(this.config.enabledProviders)\n const wasEnabled = current.has(addonId)\n if (enabled) current.add(addonId); else current.delete(addonId)\n if (wasEnabled === enabled) return\n this.ctx.logger.info('remote-access intent updated', {\n meta: { addonId, enabled },\n })\n await this.updateGlobalSettings({ enabledProviders: [...current] })\n }\n\n private resolveImpl(addonId: string): NetworkAccessLike | null {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike>(\n 'network-access',\n ) ?? []\n const found = entries.find(([id]) => id === addonId)\n return found?.[1] ?? null\n }\n}\n\nexport default RemoteAccessOrchestratorAddon\n"],"mappings":"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"}
|