@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.
Files changed (82) hide show
  1. package/dist/auth/auth-manager.d.ts +12 -1
  2. package/dist/auth/auth-manager.d.ts.map +1 -1
  3. package/dist/auth/scope-matcher.d.ts +8 -0
  4. package/dist/auth/scope-matcher.d.ts.map +1 -0
  5. package/dist/auth/totp-manager.d.ts +0 -1
  6. package/dist/auth/totp-manager.d.ts.map +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +15 -0
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts.map +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +27 -6
  10. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -1
  11. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +27 -6
  12. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -1
  13. package/dist/builtins/device-manager/device-config-contribution.d.ts +33 -0
  14. package/dist/builtins/device-manager/device-config-contribution.d.ts.map +1 -0
  15. package/dist/builtins/device-manager/device-manager.addon.d.ts +52 -17
  16. package/dist/builtins/device-manager/device-manager.addon.d.ts.map +1 -1
  17. package/dist/builtins/device-manager/device-manager.addon.js +285 -161
  18. package/dist/builtins/device-manager/device-manager.addon.js.map +1 -1
  19. package/dist/builtins/device-manager/device-manager.addon.mjs +286 -162
  20. package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -1
  21. package/dist/builtins/local-auth/auth-schema.d.ts +1 -0
  22. package/dist/builtins/local-auth/auth-schema.d.ts.map +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.d.ts +1 -0
  24. package/dist/builtins/local-auth/local-auth.addon.d.ts.map +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +354 -3
  26. package/dist/builtins/local-auth/local-auth.addon.js.map +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.mjs +355 -3
  28. package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -1
  29. package/dist/builtins/local-auth/oauth-grants.d.ts +46 -0
  30. package/dist/builtins/local-auth/oauth-grants.d.ts.map +1 -0
  31. package/dist/builtins/local-auth/oauth-session-manager.d.ts +51 -0
  32. package/dist/builtins/local-auth/oauth-session-manager.d.ts.map +1 -0
  33. package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts +97 -0
  34. package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts.map +1 -0
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +24 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts.map +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +136 -56
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js.map +1 -1
  39. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +137 -57
  40. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs.map +1 -1
  41. package/dist/builtins/snapshot/index.js +1 -3
  42. package/dist/builtins/snapshot/index.js.map +1 -1
  43. package/dist/builtins/snapshot/index.mjs +1 -3
  44. package/dist/builtins/snapshot/index.mjs.map +1 -1
  45. package/dist/builtins/snapshot/snapshot.addon.d.ts.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +428 -234
  49. package/dist/index.js.map +1 -1
  50. package/dist/index.mjs +428 -235
  51. package/dist/index.mjs.map +1 -1
  52. package/package.json +19 -37
  53. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts +0 -8
  54. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts.map +0 -1
  55. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js +0 -75
  56. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js.map +0 -1
  57. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs +0 -69
  58. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs.map +0 -1
  59. package/dist/builtins/auth-orchestrator/index.d.ts +0 -2
  60. package/dist/builtins/auth-orchestrator/index.d.ts.map +0 -1
  61. package/dist/builtins/auth-orchestrator/index.js +0 -7
  62. package/dist/builtins/auth-orchestrator/index.mjs +0 -2
  63. package/dist/builtins/mesh-orchestrator/index.d.ts +0 -2
  64. package/dist/builtins/mesh-orchestrator/index.d.ts.map +0 -1
  65. package/dist/builtins/mesh-orchestrator/index.js +0 -7
  66. package/dist/builtins/mesh-orchestrator/index.mjs +0 -2
  67. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts +0 -9
  68. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts.map +0 -1
  69. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js +0 -113
  70. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js.map +0 -1
  71. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs +0 -107
  72. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs.map +0 -1
  73. package/dist/builtins/turn-orchestrator/index.d.ts +0 -2
  74. package/dist/builtins/turn-orchestrator/index.d.ts.map +0 -1
  75. package/dist/builtins/turn-orchestrator/index.js +0 -7
  76. package/dist/builtins/turn-orchestrator/index.mjs +0 -2
  77. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts +0 -34
  78. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts.map +0 -1
  79. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js +0 -126
  80. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js.map +0 -1
  81. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs +0 -120
  82. 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;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,SAAS,EAIT,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAA;AAgBxB,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;YAyDjD,yBAAyB;YA8CzB,WAAW;IAQzB,OAAO,CAAC,WAAW;YAQL,aAAa;CAkC5B;AAED,eAAe,6BAA6B,CAAA"}
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 — singleton facade over the
10
- * `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).
11
- * Mirrors the auth-orchestrator and backup-orchestrator patterns.
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
- * Persistence + autostart contract:
47
+ * What it still owns — the load-bearing logic:
14
48
  * The orchestrator owns the "operator wants this provider running"
15
- * intent — a `enabledProviders: string[]` slice in its addon-store
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. `startProvider` /
18
- * `stopProvider` mutate this list so a Start press persists across
19
- * restarts. Same shape as turn-orchestrator's setProviderEnabled.
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
- const provider = {
27
- listProviders: async () => this.listProviders(),
28
- startProvider: async ({ addonId }) => {
29
- const impl = this.resolveImpl(addonId);
30
- if (!impl?.start) throw new Error(`Remote-access provider "${addonId}" does not support start`);
31
- const endpoint = await impl.start();
32
- await this.markEnabled(addonId, true);
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
- capability: _camstack_types.remoteAccessCapability,
54
- provider
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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"}