@pellux/goodvibes-sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/artifacts/operator-contract.json +1320 -10
- package/dist/events/communication.d.ts +1 -1
- package/dist/platform/agents/orchestrator-runner.d.ts +5 -5
- package/dist/platform/agents/orchestrator-runner.js +6 -6
- package/dist/platform/agents/orchestrator.d.ts +4 -4
- package/dist/platform/agents/orchestrator.js +2 -2
- package/dist/platform/agents/wrfc-controller.d.ts +1 -1
- package/dist/platform/agents/wrfc-controller.js +3 -3
- package/dist/platform/calendar/index.d.ts +2 -2
- package/dist/platform/calendar/index.js +3 -3
- package/dist/platform/calendar/oauth-providers.d.ts +1 -1
- package/dist/platform/calendar/oauth-providers.js +1 -1
- package/dist/platform/calendar/oauth-types.d.ts +1 -1
- package/dist/platform/calendar/oauth-types.js +1 -1
- package/dist/platform/companion/companion-chat-branching.d.ts +66 -0
- package/dist/platform/companion/companion-chat-branching.d.ts.map +1 -0
- package/dist/platform/companion/companion-chat-branching.js +142 -0
- package/dist/platform/companion/companion-chat-broker-bridge.d.ts +1 -1
- package/dist/platform/companion/companion-chat-broker-bridge.d.ts.map +1 -1
- package/dist/platform/companion/companion-chat-broker-sync.d.ts +2 -2
- package/dist/platform/companion/companion-chat-broker-sync.d.ts.map +1 -1
- package/dist/platform/companion/companion-chat-broker-sync.js +1 -1
- package/dist/platform/companion/companion-chat-manager.d.ts +26 -5
- package/dist/platform/companion/companion-chat-manager.d.ts.map +1 -1
- package/dist/platform/companion/companion-chat-manager.js +67 -81
- package/dist/platform/companion/companion-chat-routes.d.ts.map +1 -1
- package/dist/platform/companion/companion-chat-routes.js +77 -1
- package/dist/platform/companion/companion-chat-turn-execution.d.ts +61 -0
- package/dist/platform/companion/companion-chat-turn-execution.d.ts.map +1 -0
- package/dist/platform/companion/companion-chat-turn-execution.js +107 -0
- package/dist/platform/companion/companion-chat-types.d.ts +67 -0
- package/dist/platform/companion/companion-chat-types.d.ts.map +1 -1
- package/dist/platform/config/credential-status.d.ts +1 -1
- package/dist/platform/config/credential-status.js +1 -1
- package/dist/platform/config/index.d.ts +1 -1
- package/dist/platform/config/index.js +1 -1
- package/dist/platform/config/manager.d.ts +1 -1
- package/dist/platform/config/manager.js +1 -1
- package/dist/platform/config/migrations.d.ts +2 -2
- package/dist/platform/config/migrations.js +2 -2
- package/dist/platform/config/schema-domain-runtime.d.ts +8 -0
- package/dist/platform/config/schema-domain-runtime.d.ts.map +1 -1
- package/dist/platform/config/schema-domain-runtime.js +32 -0
- package/dist/platform/config/schema-types.d.ts +10 -2
- package/dist/platform/config/schema-types.d.ts.map +1 -1
- package/dist/platform/control-plane/gateway-scope-enforcement.js +2 -2
- package/dist/platform/control-plane/index.d.ts +2 -0
- package/dist/platform/control-plane/index.d.ts.map +1 -1
- package/dist/platform/control-plane/index.js +5 -1
- package/dist/platform/control-plane/invoke-input-validation.d.ts +1 -1
- package/dist/platform/control-plane/invoke-input-validation.js +1 -1
- package/dist/platform/control-plane/method-catalog-calendar.d.ts +2 -2
- package/dist/platform/control-plane/method-catalog-calendar.js +2 -2
- package/dist/platform/control-plane/method-catalog-channels.js +3 -3
- package/dist/platform/control-plane/method-catalog-control-automation.js +4 -4
- package/dist/platform/control-plane/method-catalog-control-companion.d.ts +1 -1
- package/dist/platform/control-plane/method-catalog-control-companion.d.ts.map +1 -1
- package/dist/platform/control-plane/method-catalog-control-companion.js +43 -0
- package/dist/platform/control-plane/method-catalog-control-core.js +3 -3
- package/dist/platform/control-plane/method-catalog-email.d.ts +1 -1
- package/dist/platform/control-plane/method-catalog-email.js +1 -1
- package/dist/platform/control-plane/method-catalog-fleet.d.ts +1 -1
- package/dist/platform/control-plane/method-catalog-push.d.ts +30 -0
- package/dist/platform/control-plane/method-catalog-push.d.ts.map +1 -0
- package/dist/platform/control-plane/method-catalog-push.js +80 -0
- package/dist/platform/control-plane/method-catalog-route-reconcile.d.ts +1 -1
- package/dist/platform/control-plane/method-catalog-runtime.d.ts.map +1 -1
- package/dist/platform/control-plane/method-catalog-runtime.js +83 -1
- package/dist/platform/control-plane/method-catalog.d.ts.map +1 -1
- package/dist/platform/control-plane/method-catalog.js +2 -0
- package/dist/platform/control-plane/operator-contract-schemas-fleet.js +1 -1
- package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts +8 -0
- package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts.map +1 -1
- package/dist/platform/control-plane/operator-contract-schemas-runtime.js +43 -0
- package/dist/platform/control-plane/routes/checkpoints.d.ts +1 -1
- package/dist/platform/control-plane/routes/fleet.d.ts +2 -2
- package/dist/platform/control-plane/routes/fleet.js +1 -1
- package/dist/platform/control-plane/routes/gateway-verb-error.d.ts +1 -1
- package/dist/platform/control-plane/routes/gateway-verb-error.js +1 -1
- package/dist/platform/control-plane/routes/index.js +1 -1
- package/dist/platform/control-plane/routes/invocation-params.d.ts +1 -1
- package/dist/platform/control-plane/routes/push.d.ts +26 -0
- package/dist/platform/control-plane/routes/push.d.ts.map +1 -0
- package/dist/platform/control-plane/routes/push.js +104 -0
- package/dist/platform/control-plane/routes/register-gateway-verb-groups.d.ts +31 -0
- package/dist/platform/control-plane/routes/register-gateway-verb-groups.d.ts.map +1 -0
- package/dist/platform/control-plane/routes/register-gateway-verb-groups.js +16 -0
- package/dist/platform/control-plane/routes/register-w3-s2.d.ts +2 -2
- package/dist/platform/control-plane/routes/session-search.d.ts +4 -4
- package/dist/platform/control-plane/session-broker.d.ts +1 -1
- package/dist/platform/control-plane/session-broker.js +1 -1
- package/dist/platform/control-plane/session-store-importer.d.ts +1 -1
- package/dist/platform/control-plane/session-store-importer.js +1 -1
- package/dist/platform/control-plane/session-types.d.ts +1 -1
- package/dist/platform/core/orchestrator-runtime.d.ts +4 -4
- package/dist/platform/core/orchestrator-tool-runtime.d.ts +1 -1
- package/dist/platform/core/orchestrator-turn-loop.d.ts +6 -6
- package/dist/platform/core/orchestrator-turn-loop.d.ts.map +1 -1
- package/dist/platform/core/orchestrator-turn-loop.js +23 -22
- package/dist/platform/core/orchestrator.d.ts +10 -10
- package/dist/platform/core/orchestrator.d.ts.map +1 -1
- package/dist/platform/core/orchestrator.js +8 -8
- package/dist/platform/daemon/boot.d.ts +6 -0
- package/dist/platform/daemon/boot.d.ts.map +1 -1
- package/dist/platform/daemon/boot.js +1 -0
- package/dist/platform/daemon/control-plane.js +2 -2
- package/dist/platform/daemon/facade.d.ts +4 -7
- package/dist/platform/daemon/facade.d.ts.map +1 -1
- package/dist/platform/daemon/facade.js +5 -7
- package/dist/platform/daemon/http/router-session-broker-adapter.d.ts +1 -1
- package/dist/platform/daemon/http/router.d.ts +1 -0
- package/dist/platform/daemon/http/router.d.ts.map +1 -1
- package/dist/platform/daemon/http/router.js +36 -19
- package/dist/platform/daemon/http/runtime-route-types.d.ts +1 -1
- package/dist/platform/daemon/http/runtime-route-types.d.ts.map +1 -1
- package/dist/platform/daemon/http/webui-serving.d.ts +68 -0
- package/dist/platform/daemon/http/webui-serving.d.ts.map +1 -0
- package/dist/platform/daemon/http/webui-serving.js +230 -0
- package/dist/platform/knowledge/knowledge-api.d.ts +1 -1
- package/dist/platform/orchestration/budget.d.ts +2 -2
- package/dist/platform/orchestration/cancellation.d.ts +2 -2
- package/dist/platform/orchestration/controller-compat.d.ts +2 -3
- package/dist/platform/orchestration/controller-compat.d.ts.map +1 -1
- package/dist/platform/orchestration/dirty-guard.js +1 -1
- package/dist/platform/orchestration/engine.d.ts.map +1 -1
- package/dist/platform/orchestration/engine.js +3 -4
- package/dist/platform/orchestration/persistence.js +2 -2
- package/dist/platform/orchestration/phase-runner.d.ts +4 -4
- package/dist/platform/orchestration/scheduler.d.ts +1 -1
- package/dist/platform/orchestration/types.d.ts +3 -3
- package/dist/platform/presentation/glyphs.d.ts +1 -1
- package/dist/platform/presentation/glyphs.js +1 -1
- package/dist/platform/presentation/index.d.ts +3 -3
- package/dist/platform/presentation/index.js +3 -3
- package/dist/platform/presentation/thinking-phrases.d.ts +1 -1
- package/dist/platform/presentation/thinking-phrases.js +1 -1
- package/dist/platform/presentation/waiting-wording.d.ts +1 -1
- package/dist/platform/presentation/waiting-wording.js +1 -1
- package/dist/platform/push/delivery.d.ts +48 -0
- package/dist/platform/push/delivery.d.ts.map +1 -0
- package/dist/platform/push/delivery.js +117 -0
- package/dist/platform/push/encryption.d.ts +41 -0
- package/dist/platform/push/encryption.d.ts.map +1 -0
- package/dist/platform/push/encryption.js +82 -0
- package/dist/platform/push/index.d.ts +17 -0
- package/dist/platform/push/index.d.ts.map +1 -0
- package/dist/platform/push/index.js +10 -0
- package/dist/platform/push/service.d.ts +75 -0
- package/dist/platform/push/service.d.ts.map +1 -0
- package/dist/platform/push/service.js +95 -0
- package/dist/platform/push/subscription-store.d.ts +50 -0
- package/dist/platform/push/subscription-store.d.ts.map +1 -0
- package/dist/platform/push/subscription-store.js +123 -0
- package/dist/platform/push/types.d.ts +68 -0
- package/dist/platform/push/types.d.ts.map +1 -0
- package/dist/platform/push/types.js +13 -0
- package/dist/platform/push/vapid.d.ts +54 -0
- package/dist/platform/push/vapid.d.ts.map +1 -0
- package/dist/platform/push/vapid.js +113 -0
- package/dist/platform/runtime/feature-flags/flags.js +3 -3
- package/dist/platform/runtime/fleet/adapters/agent.d.ts +2 -2
- package/dist/platform/runtime/fleet/adapters/agent.d.ts.map +1 -1
- package/dist/platform/runtime/fleet/adapters/agent.js +4 -4
- package/dist/platform/runtime/fleet/adapters/automation.d.ts +1 -1
- package/dist/platform/runtime/fleet/adapters/automation.js +1 -1
- package/dist/platform/runtime/fleet/adapters/code-index.d.ts +1 -1
- package/dist/platform/runtime/fleet/adapters/code-index.js +1 -1
- package/dist/platform/runtime/fleet/adapters/orchestration.js +1 -1
- package/dist/platform/runtime/fleet/adapters/schedule.d.ts +4 -4
- package/dist/platform/runtime/fleet/adapters/schedule.js +4 -4
- package/dist/platform/runtime/fleet/adapters/trigger.d.ts +3 -4
- package/dist/platform/runtime/fleet/adapters/trigger.d.ts.map +1 -1
- package/dist/platform/runtime/fleet/adapters/trigger.js +3 -4
- package/dist/platform/runtime/fleet/adapters/wrfc.js +1 -1
- package/dist/platform/runtime/fleet/registry.d.ts +7 -7
- package/dist/platform/runtime/fleet/registry.d.ts.map +1 -1
- package/dist/platform/runtime/fleet/registry.js +3 -3
- package/dist/platform/runtime/fleet/types.d.ts +9 -9
- package/dist/platform/runtime/fleet/types.d.ts.map +1 -1
- package/dist/platform/runtime/memory-spine/client.d.ts +127 -0
- package/dist/platform/runtime/memory-spine/client.d.ts.map +1 -0
- package/dist/platform/runtime/memory-spine/client.js +113 -0
- package/dist/platform/runtime/memory-spine/index.d.ts +11 -0
- package/dist/platform/runtime/memory-spine/index.d.ts.map +1 -0
- package/dist/platform/runtime/memory-spine/index.js +10 -0
- package/dist/platform/runtime/operator-client.d.ts +1 -1
- package/dist/platform/runtime/services.d.ts +6 -6
- package/dist/platform/runtime/services.d.ts.map +1 -1
- package/dist/platform/runtime/services.js +9 -9
- package/dist/platform/runtime/session-spine/union-cache.d.ts +1 -1
- package/dist/platform/runtime/session-spine/union-cache.js +1 -1
- package/dist/platform/state/canonical-memory.d.ts +7 -7
- package/dist/platform/state/canonical-memory.js +8 -8
- package/dist/platform/state/code-index-chunking.js +1 -1
- package/dist/platform/state/code-index-db.d.ts +1 -1
- package/dist/platform/state/code-index-store.js +1 -1
- package/dist/platform/state/index.d.ts +2 -2
- package/dist/platform/state/index.d.ts.map +1 -1
- package/dist/platform/state/index.js +1 -1
- package/dist/platform/state/memory-recall-contract.d.ts +59 -4
- package/dist/platform/state/memory-recall-contract.d.ts.map +1 -1
- package/dist/platform/state/memory-recall-contract.js +79 -3
- package/dist/platform/state/memory-registry.d.ts +8 -0
- package/dist/platform/state/memory-registry.d.ts.map +1 -1
- package/dist/platform/state/memory-registry.js +10 -0
- package/dist/platform/state/memory-vector-store.js +1 -1
- package/dist/platform/state/sqlite-vec-loader.d.ts +1 -1
- package/dist/platform/state/sqlite-vec-loader.js +1 -1
- package/dist/platform/state/vibe-projection.d.ts +3 -3
- package/dist/platform/state/vibe-projection.js +3 -3
- package/dist/platform/tools/agent/manager.d.ts +10 -10
- package/dist/platform/tools/agent/manager.js +7 -7
- package/dist/platform/tools/exec/runtime.js +5 -5
- package/dist/platform/tools/exec/schema.d.ts +1 -1
- package/dist/platform/tools/exec/schema.d.ts.map +1 -1
- package/dist/platform/tools/fetch/runtime.d.ts +1 -1
- package/dist/platform/tools/registry.d.ts +1 -1
- package/dist/platform/tools/registry.js +1 -1
- package/dist/platform/types/tools.d.ts +1 -1
- package/dist/platform/utils/request-body.d.ts.map +1 -1
- package/dist/platform/utils/request-body.js +50 -5
- package/dist/platform/version.js +1 -1
- package/package.json +13 -9
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/service.ts
|
|
3
|
+
*
|
|
4
|
+
* PushService — the seam the gateway verbs and the daemon event sources both
|
|
5
|
+
* talk to. It owns the subscription store, the VAPID key custody, and the
|
|
6
|
+
* delivery path, and it turns a real daemon event (an approval that needs a
|
|
7
|
+
* decision) into a fan-out of encrypted pushes.
|
|
8
|
+
*
|
|
9
|
+
* Honest-degrade posture, end to end:
|
|
10
|
+
* - No subscriptions -> `deliver()` returns an empty receipt list; nothing is
|
|
11
|
+
* faked as sent.
|
|
12
|
+
* - A subscription whose endpoint is gone (404/410) is pruned by the delivery
|
|
13
|
+
* path and reported as `pruned`.
|
|
14
|
+
* - VAPID keys are minted lazily on first real need; a daemon that never uses
|
|
15
|
+
* push never generates or stores a key.
|
|
16
|
+
*/
|
|
17
|
+
import { type PushTransport } from './delivery.js';
|
|
18
|
+
import { PushSubscriptionStore } from './subscription-store.js';
|
|
19
|
+
import { VapidManager } from './vapid.js';
|
|
20
|
+
import type { PublicPushSubscription, PushDeliveryReceipt, PushMessage, SubscriptionKeyMaterial } from './types.js';
|
|
21
|
+
/** The slice of the approval broker the push delivery source subscribes to. */
|
|
22
|
+
export interface ApprovalSource {
|
|
23
|
+
subscribe(listener: (approval: ApprovalNotice) => void): () => void;
|
|
24
|
+
}
|
|
25
|
+
/** The minimum an approval record needs to expose to become a push. */
|
|
26
|
+
export interface ApprovalNotice {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly status: string;
|
|
29
|
+
readonly request?: {
|
|
30
|
+
readonly tool?: string;
|
|
31
|
+
readonly analysis?: {
|
|
32
|
+
readonly summary?: string;
|
|
33
|
+
};
|
|
34
|
+
} | undefined;
|
|
35
|
+
}
|
|
36
|
+
export interface PushServiceDeps {
|
|
37
|
+
readonly vapid: VapidManager;
|
|
38
|
+
readonly store: PushSubscriptionStore;
|
|
39
|
+
/** Overridable delivery transport; production uses the built-in fetch. */
|
|
40
|
+
readonly transport?: PushTransport | undefined;
|
|
41
|
+
}
|
|
42
|
+
export interface SubscribeInput {
|
|
43
|
+
readonly principalId: string;
|
|
44
|
+
readonly endpoint: string;
|
|
45
|
+
readonly keys: SubscriptionKeyMaterial;
|
|
46
|
+
}
|
|
47
|
+
export declare class PushService {
|
|
48
|
+
private readonly vapid;
|
|
49
|
+
private readonly store;
|
|
50
|
+
private readonly transport?;
|
|
51
|
+
/** Approval ids already pushed, so re-publishes (claim/approve) don't re-notify. */
|
|
52
|
+
private readonly notifiedApprovals;
|
|
53
|
+
constructor(deps: PushServiceDeps);
|
|
54
|
+
/** The public VAPID key clients subscribe with. */
|
|
55
|
+
getPublicKey(): Promise<string>;
|
|
56
|
+
subscribe(input: SubscribeInput): Promise<PublicPushSubscription>;
|
|
57
|
+
listSubscriptions(principalId: string): Promise<PublicPushSubscription[]>;
|
|
58
|
+
/** Delete a subscription for a principal. False when the id was already absent. */
|
|
59
|
+
unsubscribe(id: string, principalId: string): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Send a test push to one of the principal's subscriptions and return the
|
|
62
|
+
* honest receipt (delivered / pruned / failed). Returns null when the id is
|
|
63
|
+
* not a subscription owned by this principal, so the verb can 404.
|
|
64
|
+
*/
|
|
65
|
+
verify(id: string, principalId: string): Promise<PushDeliveryReceipt | null>;
|
|
66
|
+
/** Fan a message out to every stored subscription. */
|
|
67
|
+
deliver(message: PushMessage): Promise<PushDeliveryReceipt[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Wire a real event source: when an approval is created (status `pending`),
|
|
70
|
+
* push it to every registered device. Later re-publishes of the same approval
|
|
71
|
+
* (claimed/approved/denied) do not re-notify. Returns an unsubscribe handle.
|
|
72
|
+
*/
|
|
73
|
+
attachApprovalSource(source: ApprovalSource): () => void;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/platform/push/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAuC,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAwB,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EACV,sBAAsB,EACtB,mBAAmB,EACnB,WAAW,EACX,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACrE;AAED,uEAAuE;AACvE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;YAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,SAAS,CAAC;CAC9G;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC;IACtC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;CACxC;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAA4B;IACvD,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;gBAE3C,IAAI,EAAE,eAAe;IAMjC,mDAAmD;IACnD,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAIzB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAKvE,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAIzE,mFAAmF;IACnF,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI9D;;;;OAIG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAclF,sDAAsD;IACtD,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAI7D;;;;OAIG;IACH,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,IAAI;CAsBzD"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/service.ts
|
|
3
|
+
*
|
|
4
|
+
* PushService — the seam the gateway verbs and the daemon event sources both
|
|
5
|
+
* talk to. It owns the subscription store, the VAPID key custody, and the
|
|
6
|
+
* delivery path, and it turns a real daemon event (an approval that needs a
|
|
7
|
+
* decision) into a fan-out of encrypted pushes.
|
|
8
|
+
*
|
|
9
|
+
* Honest-degrade posture, end to end:
|
|
10
|
+
* - No subscriptions -> `deliver()` returns an empty receipt list; nothing is
|
|
11
|
+
* faked as sent.
|
|
12
|
+
* - A subscription whose endpoint is gone (404/410) is pruned by the delivery
|
|
13
|
+
* path and reported as `pruned`.
|
|
14
|
+
* - VAPID keys are minted lazily on first real need; a daemon that never uses
|
|
15
|
+
* push never generates or stores a key.
|
|
16
|
+
*/
|
|
17
|
+
import { logger } from '../utils/logger.js';
|
|
18
|
+
import { deliverToAll, deliverToSubscription } from './delivery.js';
|
|
19
|
+
import { PushSubscriptionStore, toPublicSubscription } from './subscription-store.js';
|
|
20
|
+
import { VapidManager } from './vapid.js';
|
|
21
|
+
export class PushService {
|
|
22
|
+
vapid;
|
|
23
|
+
store;
|
|
24
|
+
transport;
|
|
25
|
+
/** Approval ids already pushed, so re-publishes (claim/approve) don't re-notify. */
|
|
26
|
+
notifiedApprovals = new Set();
|
|
27
|
+
constructor(deps) {
|
|
28
|
+
this.vapid = deps.vapid;
|
|
29
|
+
this.store = deps.store;
|
|
30
|
+
this.transport = deps.transport;
|
|
31
|
+
}
|
|
32
|
+
/** The public VAPID key clients subscribe with. */
|
|
33
|
+
getPublicKey() {
|
|
34
|
+
return this.vapid.getPublicKey();
|
|
35
|
+
}
|
|
36
|
+
async subscribe(input) {
|
|
37
|
+
const record = await this.store.register(input);
|
|
38
|
+
return toPublicSubscription(record);
|
|
39
|
+
}
|
|
40
|
+
listSubscriptions(principalId) {
|
|
41
|
+
return this.store.listPublic(principalId);
|
|
42
|
+
}
|
|
43
|
+
/** Delete a subscription for a principal. False when the id was already absent. */
|
|
44
|
+
unsubscribe(id, principalId) {
|
|
45
|
+
return this.store.remove(id, principalId);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Send a test push to one of the principal's subscriptions and return the
|
|
49
|
+
* honest receipt (delivered / pruned / failed). Returns null when the id is
|
|
50
|
+
* not a subscription owned by this principal, so the verb can 404.
|
|
51
|
+
*/
|
|
52
|
+
async verify(id, principalId) {
|
|
53
|
+
const record = await this.store.get(id);
|
|
54
|
+
if (!record || record.principalId !== principalId)
|
|
55
|
+
return null;
|
|
56
|
+
return deliverToSubscription(record, {
|
|
57
|
+
title: 'GoodVibes test notification',
|
|
58
|
+
body: 'Browser push is wired up and working.',
|
|
59
|
+
urgency: 'normal',
|
|
60
|
+
}, { vapid: this.vapid, store: this.store, transport: this.transport });
|
|
61
|
+
}
|
|
62
|
+
/** Fan a message out to every stored subscription. */
|
|
63
|
+
deliver(message) {
|
|
64
|
+
return deliverToAll(message, { vapid: this.vapid, store: this.store, transport: this.transport });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wire a real event source: when an approval is created (status `pending`),
|
|
68
|
+
* push it to every registered device. Later re-publishes of the same approval
|
|
69
|
+
* (claimed/approved/denied) do not re-notify. Returns an unsubscribe handle.
|
|
70
|
+
*/
|
|
71
|
+
attachApprovalSource(source) {
|
|
72
|
+
return source.subscribe((approval) => {
|
|
73
|
+
if (approval.status !== 'pending')
|
|
74
|
+
return;
|
|
75
|
+
if (this.notifiedApprovals.has(approval.id))
|
|
76
|
+
return;
|
|
77
|
+
this.notifiedApprovals.add(approval.id);
|
|
78
|
+
const tool = approval.request?.tool ?? 'a tool';
|
|
79
|
+
const summary = approval.request?.analysis?.summary ?? 'A pending action needs your decision.';
|
|
80
|
+
// Fire-and-forget: an approval must never block on a push, and a delivery
|
|
81
|
+
// failure is already captured as an honest receipt/outcome inside deliver().
|
|
82
|
+
void this.deliver({
|
|
83
|
+
title: 'Approval required',
|
|
84
|
+
body: `${tool}: ${summary}`,
|
|
85
|
+
data: { kind: 'approval', approvalId: approval.id },
|
|
86
|
+
urgency: 'high',
|
|
87
|
+
}).catch((error) => {
|
|
88
|
+
logger.warn('PushService: approval fan-out failed', {
|
|
89
|
+
approvalId: approval.id,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/subscription-store.ts
|
|
3
|
+
*
|
|
4
|
+
* The on-disk record of which devices an operator has registered for browser
|
|
5
|
+
* push. Persisted with the same atomic-JSON `PersistentStore` the approval and
|
|
6
|
+
* session stores use — the capability URLs and key material stay on disk in the
|
|
7
|
+
* daemon's own state directory, never on the wire.
|
|
8
|
+
*
|
|
9
|
+
* Delete means delete: `remove()` drops the record entirely (it does not flag a
|
|
10
|
+
* tombstone), and `list()` cannot return it afterward. Pruning a dead endpoint
|
|
11
|
+
* uses the same `remove()` path.
|
|
12
|
+
*/
|
|
13
|
+
import type { PublicPushSubscription, StoredPushSubscription, SubscriptionKeyMaterial } from './types.js';
|
|
14
|
+
export interface RegisterSubscriptionInput {
|
|
15
|
+
readonly principalId: string;
|
|
16
|
+
readonly endpoint: string;
|
|
17
|
+
readonly keys: SubscriptionKeyMaterial;
|
|
18
|
+
}
|
|
19
|
+
/** The redacted, wire-safe projection of a stored subscription. */
|
|
20
|
+
export declare function toPublicSubscription(record: StoredPushSubscription): PublicPushSubscription;
|
|
21
|
+
export declare class PushSubscriptionStore {
|
|
22
|
+
private readonly store;
|
|
23
|
+
private records;
|
|
24
|
+
constructor(filePath: string);
|
|
25
|
+
private ensureLoaded;
|
|
26
|
+
private flush;
|
|
27
|
+
/**
|
|
28
|
+
* Register (or refresh) a subscription. Two subscriptions from the same
|
|
29
|
+
* principal for the same endpoint collapse onto one record — a browser that
|
|
30
|
+
* re-subscribes with rotated keys updates in place rather than piling up
|
|
31
|
+
* duplicate capability URLs.
|
|
32
|
+
*/
|
|
33
|
+
register(input: RegisterSubscriptionInput): Promise<StoredPushSubscription>;
|
|
34
|
+
/** All subscriptions for a principal, redacted for the wire. */
|
|
35
|
+
listPublic(principalId: string): Promise<PublicPushSubscription[]>;
|
|
36
|
+
/** The full record (endpoint + keys) for a delivery. Not for the wire. */
|
|
37
|
+
get(id: string): Promise<StoredPushSubscription | null>;
|
|
38
|
+
/** Every stored subscription — the delivery fan-out reads this. Not for the wire. */
|
|
39
|
+
all(): Promise<readonly StoredPushSubscription[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Delete a subscription. Returns true if a record was actually removed, false
|
|
42
|
+
* if the id was already absent — the caller reports an honest 404 rather than
|
|
43
|
+
* a 200-noop. An optional `principalId` scopes the delete so one operator
|
|
44
|
+
* cannot remove another's device.
|
|
45
|
+
*/
|
|
46
|
+
remove(id: string, principalId?: string): Promise<boolean>;
|
|
47
|
+
/** Record the outcome of the last delivery attempt against a subscription. */
|
|
48
|
+
recordOutcome(id: string, outcome: StoredPushSubscription['lastOutcome']): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=subscription-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription-store.d.ts","sourceRoot":"","sources":["../../../src/platform/push/subscription-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;CACxC;AAcD,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,sBAAsB,GAAG,sBAAsB,CAU3F;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAC9D,OAAO,CAAC,OAAO,CAAyC;gBAE5C,QAAQ,EAAE,MAAM;YAId,YAAY;YAOZ,KAAK;IAInB;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,yBAAyB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAyBjF,gEAAgE;IAC1D,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAOxE,0EAA0E;IACpE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAK7D,qFAAqF;IAC/E,GAAG,IAAI,OAAO,CAAC,SAAS,sBAAsB,EAAE,CAAC;IAIvD;;;;;OAKG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWhE,8EAA8E;IACxE,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ/F"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/subscription-store.ts
|
|
3
|
+
*
|
|
4
|
+
* The on-disk record of which devices an operator has registered for browser
|
|
5
|
+
* push. Persisted with the same atomic-JSON `PersistentStore` the approval and
|
|
6
|
+
* session stores use — the capability URLs and key material stay on disk in the
|
|
7
|
+
* daemon's own state directory, never on the wire.
|
|
8
|
+
*
|
|
9
|
+
* Delete means delete: `remove()` drops the record entirely (it does not flag a
|
|
10
|
+
* tombstone), and `list()` cannot return it afterward. Pruning a dead endpoint
|
|
11
|
+
* uses the same `remove()` path.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
14
|
+
import { PersistentStore } from '../state/persistent-store.js';
|
|
15
|
+
function endpointOrigin(endpoint) {
|
|
16
|
+
try {
|
|
17
|
+
return new URL(endpoint).origin;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return 'invalid';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function endpointHash(endpoint) {
|
|
24
|
+
return createHash('sha256').update(endpoint).digest('base64url').slice(0, 16);
|
|
25
|
+
}
|
|
26
|
+
/** The redacted, wire-safe projection of a stored subscription. */
|
|
27
|
+
export function toPublicSubscription(record) {
|
|
28
|
+
return {
|
|
29
|
+
id: record.id,
|
|
30
|
+
principalId: record.principalId,
|
|
31
|
+
endpointOrigin: endpointOrigin(record.endpoint),
|
|
32
|
+
endpointHash: endpointHash(record.endpoint),
|
|
33
|
+
createdAt: record.createdAt,
|
|
34
|
+
lastDeliveryAt: record.lastDeliveryAt,
|
|
35
|
+
lastOutcome: record.lastOutcome,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export class PushSubscriptionStore {
|
|
39
|
+
store;
|
|
40
|
+
records = null;
|
|
41
|
+
constructor(filePath) {
|
|
42
|
+
this.store = new PersistentStore(filePath);
|
|
43
|
+
}
|
|
44
|
+
async ensureLoaded() {
|
|
45
|
+
if (this.records)
|
|
46
|
+
return this.records;
|
|
47
|
+
const snapshot = await this.store.load();
|
|
48
|
+
this.records = snapshot?.subscriptions ? [...snapshot.subscriptions] : [];
|
|
49
|
+
return this.records;
|
|
50
|
+
}
|
|
51
|
+
async flush() {
|
|
52
|
+
await this.store.persist({ subscriptions: this.records ?? [] });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Register (or refresh) a subscription. Two subscriptions from the same
|
|
56
|
+
* principal for the same endpoint collapse onto one record — a browser that
|
|
57
|
+
* re-subscribes with rotated keys updates in place rather than piling up
|
|
58
|
+
* duplicate capability URLs.
|
|
59
|
+
*/
|
|
60
|
+
async register(input) {
|
|
61
|
+
const records = await this.ensureLoaded();
|
|
62
|
+
const existingIndex = records.findIndex((r) => r.principalId === input.principalId && r.endpoint === input.endpoint);
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
if (existingIndex >= 0) {
|
|
65
|
+
const prior = records[existingIndex];
|
|
66
|
+
const updated = { ...prior, keys: input.keys };
|
|
67
|
+
records[existingIndex] = updated;
|
|
68
|
+
await this.flush();
|
|
69
|
+
return updated;
|
|
70
|
+
}
|
|
71
|
+
const record = {
|
|
72
|
+
id: `push-${randomUUID()}`,
|
|
73
|
+
principalId: input.principalId,
|
|
74
|
+
endpoint: input.endpoint,
|
|
75
|
+
keys: input.keys,
|
|
76
|
+
createdAt: now,
|
|
77
|
+
};
|
|
78
|
+
records.push(record);
|
|
79
|
+
await this.flush();
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
/** All subscriptions for a principal, redacted for the wire. */
|
|
83
|
+
async listPublic(principalId) {
|
|
84
|
+
const records = await this.ensureLoaded();
|
|
85
|
+
return records
|
|
86
|
+
.filter((r) => r.principalId === principalId)
|
|
87
|
+
.map(toPublicSubscription);
|
|
88
|
+
}
|
|
89
|
+
/** The full record (endpoint + keys) for a delivery. Not for the wire. */
|
|
90
|
+
async get(id) {
|
|
91
|
+
const records = await this.ensureLoaded();
|
|
92
|
+
return records.find((r) => r.id === id) ?? null;
|
|
93
|
+
}
|
|
94
|
+
/** Every stored subscription — the delivery fan-out reads this. Not for the wire. */
|
|
95
|
+
async all() {
|
|
96
|
+
return [...(await this.ensureLoaded())];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Delete a subscription. Returns true if a record was actually removed, false
|
|
100
|
+
* if the id was already absent — the caller reports an honest 404 rather than
|
|
101
|
+
* a 200-noop. An optional `principalId` scopes the delete so one operator
|
|
102
|
+
* cannot remove another's device.
|
|
103
|
+
*/
|
|
104
|
+
async remove(id, principalId) {
|
|
105
|
+
const records = await this.ensureLoaded();
|
|
106
|
+
const index = records.findIndex((r) => r.id === id && (principalId === undefined || r.principalId === principalId));
|
|
107
|
+
if (index < 0)
|
|
108
|
+
return false;
|
|
109
|
+
records.splice(index, 1);
|
|
110
|
+
await this.flush();
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
/** Record the outcome of the last delivery attempt against a subscription. */
|
|
114
|
+
async recordOutcome(id, outcome) {
|
|
115
|
+
const records = await this.ensureLoaded();
|
|
116
|
+
const index = records.findIndex((r) => r.id === id);
|
|
117
|
+
if (index < 0)
|
|
118
|
+
return;
|
|
119
|
+
const prior = records[index];
|
|
120
|
+
records[index] = { ...prior, lastDeliveryAt: Date.now(), lastOutcome: outcome };
|
|
121
|
+
await this.flush();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/types.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared shapes for the browser-push subscription lifecycle and delivery path.
|
|
5
|
+
*
|
|
6
|
+
* Custody note: a stored subscription's `endpoint` is a capability URL (anyone
|
|
7
|
+
* holding it can push to that device) and its `keys` are the receiver's
|
|
8
|
+
* encryption material. Neither is ever returned over the wire — read verbs
|
|
9
|
+
* hand back the redacted `PublicPushSubscription` view instead. The VAPID
|
|
10
|
+
* private key never appears in any shape here at all; it lives only inside the
|
|
11
|
+
* secrets store (see push/vapid.ts).
|
|
12
|
+
*/
|
|
13
|
+
import type { SubscriptionKeyMaterial } from './encryption.js';
|
|
14
|
+
export type { SubscriptionKeyMaterial } from './encryption.js';
|
|
15
|
+
/** A subscription as stored on disk (contains capability URL + key material). */
|
|
16
|
+
export interface StoredPushSubscription {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
/** The principal (operator identity) that registered this device. */
|
|
19
|
+
readonly principalId: string;
|
|
20
|
+
/** The browser Push endpoint — a capability URL, kept off the wire. */
|
|
21
|
+
readonly endpoint: string;
|
|
22
|
+
readonly keys: SubscriptionKeyMaterial;
|
|
23
|
+
readonly createdAt: number;
|
|
24
|
+
/** Timestamp of the last delivery attempt, if any. */
|
|
25
|
+
readonly lastDeliveryAt?: number | undefined;
|
|
26
|
+
/** Outcome of the last delivery attempt, if any. */
|
|
27
|
+
readonly lastOutcome?: PushDeliveryOutcome | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The redacted, wire-safe view of a subscription. The capability URL and key
|
|
31
|
+
* material are deliberately absent; the origin + short hash are enough to
|
|
32
|
+
* identify a device in a management UI without handing the capability back out.
|
|
33
|
+
*/
|
|
34
|
+
export interface PublicPushSubscription {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly principalId: string;
|
|
37
|
+
/** The endpoint's origin only (e.g. `https://push.example`), never the full path. */
|
|
38
|
+
readonly endpointOrigin: string;
|
|
39
|
+
/** A short, stable hash of the full endpoint, for de-duplication in a UI. */
|
|
40
|
+
readonly endpointHash: string;
|
|
41
|
+
readonly createdAt: number;
|
|
42
|
+
readonly lastDeliveryAt?: number | undefined;
|
|
43
|
+
readonly lastOutcome?: PushDeliveryOutcome | undefined;
|
|
44
|
+
}
|
|
45
|
+
export type PushDeliveryOutcome = 'delivered' | 'pruned' | 'failed' | 'skipped';
|
|
46
|
+
/** Per-subscription result of a delivery fan-out, honest about what happened. */
|
|
47
|
+
export interface PushDeliveryReceipt {
|
|
48
|
+
readonly subscriptionId: string;
|
|
49
|
+
readonly endpointOrigin: string;
|
|
50
|
+
readonly outcome: PushDeliveryOutcome;
|
|
51
|
+
/** The push service's HTTP status, when a request was actually made. */
|
|
52
|
+
readonly httpStatus?: number | undefined;
|
|
53
|
+
/** Plain-language reason, especially for `pruned`/`failed`/`skipped`. */
|
|
54
|
+
readonly detail?: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Message urgency, mapped straight onto the push `Urgency` header. */
|
|
57
|
+
export type PushUrgency = 'very-low' | 'low' | 'normal' | 'high';
|
|
58
|
+
/** The notification a caller wants delivered to the operator's devices. */
|
|
59
|
+
export interface PushMessage {
|
|
60
|
+
readonly title: string;
|
|
61
|
+
readonly body: string;
|
|
62
|
+
/** Optional structured data the service worker can act on (e.g. a deep link). */
|
|
63
|
+
readonly data?: Record<string, unknown> | undefined;
|
|
64
|
+
readonly urgency?: PushUrgency | undefined;
|
|
65
|
+
/** How long (seconds) the push service should retain the message if offline. */
|
|
66
|
+
readonly ttlSeconds?: number | undefined;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/platform/push/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE/D,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE/D,iFAAiF;AACjF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,oDAAoD;IACpD,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACxD;AAED,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhF,iFAAiF;AACjF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,uEAAuE;AACvE,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjE,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/types.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared shapes for the browser-push subscription lifecycle and delivery path.
|
|
5
|
+
*
|
|
6
|
+
* Custody note: a stored subscription's `endpoint` is a capability URL (anyone
|
|
7
|
+
* holding it can push to that device) and its `keys` are the receiver's
|
|
8
|
+
* encryption material. Neither is ever returned over the wire — read verbs
|
|
9
|
+
* hand back the redacted `PublicPushSubscription` view instead. The VAPID
|
|
10
|
+
* private key never appears in any shape here at all; it lives only inside the
|
|
11
|
+
* secrets store (see push/vapid.ts).
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/vapid.ts
|
|
3
|
+
*
|
|
4
|
+
* VAPID (RFC 8292) key custody and request signing for browser push.
|
|
5
|
+
*
|
|
6
|
+
* KEY CUSTODY — the load-bearing rule of this file:
|
|
7
|
+
* - The daemon generates one P-256 keypair on first need.
|
|
8
|
+
* - The WHOLE keypair (including the private component) is persisted only
|
|
9
|
+
* through the SecretsManager, exactly like any other credential — into the
|
|
10
|
+
* secure store, or the plaintext secrets file, per the active secret policy.
|
|
11
|
+
* It is NEVER written into the config, so it can never ride out in the
|
|
12
|
+
* secret-free config snapshot.
|
|
13
|
+
* - The private key is used only here, to sign the short-lived VAPID JWT that
|
|
14
|
+
* authorizes one delivery. It is never logged and never returned by any read
|
|
15
|
+
* verb.
|
|
16
|
+
* - Only the PUBLIC key leaves the daemon: `getPublicKey()` feeds the
|
|
17
|
+
* `push.vapid.get` verb and the `k=` parameter of the Authorization header.
|
|
18
|
+
*/
|
|
19
|
+
/** The narrow slice of SecretsManager this module needs — get/set one secret. */
|
|
20
|
+
export interface VapidSecretStore {
|
|
21
|
+
get(key: string): Promise<string | null>;
|
|
22
|
+
set(key: string, value: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface VapidManagerOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The `sub` claim of the VAPID JWT — a `mailto:` or `https:` contact the push
|
|
27
|
+
* service can reach. Defaults to a local mailto when unset.
|
|
28
|
+
*/
|
|
29
|
+
readonly subject?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** The secret key under which the keypair is stored (a secrets-store key, not a config key). */
|
|
32
|
+
export declare const VAPID_SECRET_KEY = "push.vapid.keypair";
|
|
33
|
+
export declare class VapidManager {
|
|
34
|
+
private readonly store;
|
|
35
|
+
private readonly subject;
|
|
36
|
+
private inflight;
|
|
37
|
+
constructor(store: VapidSecretStore, options?: VapidManagerOptions);
|
|
38
|
+
/** The public application-server key clients subscribe with. Generates on first call. */
|
|
39
|
+
getPublicKey(): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Build the `Authorization: vapid ...` header value for one delivery to
|
|
42
|
+
* `endpoint`. The JWT's audience is the endpoint's origin (RFC 8292).
|
|
43
|
+
*/
|
|
44
|
+
buildAuthorizationHeader(endpoint: string): Promise<string>;
|
|
45
|
+
private signJwt;
|
|
46
|
+
/**
|
|
47
|
+
* Load the keypair from the secrets store, or generate and persist one on
|
|
48
|
+
* first use. Concurrent callers share a single in-flight generation so two
|
|
49
|
+
* simultaneous first-time deliveries cannot mint two keypairs.
|
|
50
|
+
*/
|
|
51
|
+
private ensureKeypair;
|
|
52
|
+
private loadOrGenerate;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=vapid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vapid.d.ts","sourceRoot":"","sources":["../../../src/platform/push/vapid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AASD,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,gGAAgG;AAChG,eAAO,MAAM,gBAAgB,uBAAuB,CAAC;AAyBrD,qBAAa,YAAY;IAIX,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHlC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAA4C;gBAE/B,KAAK,EAAE,gBAAgB,EAAE,OAAO,GAAE,mBAAwB;IAIvF,yFAAyF;IACnF,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAKrC;;;OAGG;IACG,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOjE,OAAO,CAAC,OAAO;IAkBf;;;;OAIG;IACH,OAAO,CAAC,aAAa;YAUP,cAAc;CAe7B"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push/vapid.ts
|
|
3
|
+
*
|
|
4
|
+
* VAPID (RFC 8292) key custody and request signing for browser push.
|
|
5
|
+
*
|
|
6
|
+
* KEY CUSTODY — the load-bearing rule of this file:
|
|
7
|
+
* - The daemon generates one P-256 keypair on first need.
|
|
8
|
+
* - The WHOLE keypair (including the private component) is persisted only
|
|
9
|
+
* through the SecretsManager, exactly like any other credential — into the
|
|
10
|
+
* secure store, or the plaintext secrets file, per the active secret policy.
|
|
11
|
+
* It is NEVER written into the config, so it can never ride out in the
|
|
12
|
+
* secret-free config snapshot.
|
|
13
|
+
* - The private key is used only here, to sign the short-lived VAPID JWT that
|
|
14
|
+
* authorizes one delivery. It is never logged and never returned by any read
|
|
15
|
+
* verb.
|
|
16
|
+
* - Only the PUBLIC key leaves the daemon: `getPublicKey()` feeds the
|
|
17
|
+
* `push.vapid.get` verb and the `k=` parameter of the Authorization header.
|
|
18
|
+
*/
|
|
19
|
+
import { createPrivateKey, generateKeyPairSync, sign as cryptoSign } from 'node:crypto';
|
|
20
|
+
/** The secret key under which the keypair is stored (a secrets-store key, not a config key). */
|
|
21
|
+
export const VAPID_SECRET_KEY = 'push.vapid.keypair';
|
|
22
|
+
const DEFAULT_SUBJECT = 'mailto:goodvibes-push@localhost';
|
|
23
|
+
/** VAPID JWTs are short-lived; RFC 8292 caps `exp` at 24h. Use 12h. */
|
|
24
|
+
const JWT_LIFETIME_SECONDS = 12 * 60 * 60;
|
|
25
|
+
function base64UrlFromBuffer(buffer) {
|
|
26
|
+
return buffer.toString('base64url');
|
|
27
|
+
}
|
|
28
|
+
function base64UrlJson(value) {
|
|
29
|
+
return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
|
|
30
|
+
}
|
|
31
|
+
/** Derive the 65-byte uncompressed public point (base64url) from a P-256 JWK. */
|
|
32
|
+
function publicKeyFromJwk(jwk) {
|
|
33
|
+
if (!jwk.x || !jwk.y)
|
|
34
|
+
throw new Error('VAPID JWK is missing its public coordinates');
|
|
35
|
+
const point = Buffer.concat([
|
|
36
|
+
Buffer.from([0x04]),
|
|
37
|
+
Buffer.from(jwk.x, 'base64url'),
|
|
38
|
+
Buffer.from(jwk.y, 'base64url'),
|
|
39
|
+
]);
|
|
40
|
+
return base64UrlFromBuffer(point);
|
|
41
|
+
}
|
|
42
|
+
export class VapidManager {
|
|
43
|
+
store;
|
|
44
|
+
subject;
|
|
45
|
+
inflight = null;
|
|
46
|
+
constructor(store, options = {}) {
|
|
47
|
+
this.store = store;
|
|
48
|
+
this.subject = options.subject && options.subject.length > 0 ? options.subject : DEFAULT_SUBJECT;
|
|
49
|
+
}
|
|
50
|
+
/** The public application-server key clients subscribe with. Generates on first call. */
|
|
51
|
+
async getPublicKey() {
|
|
52
|
+
const keypair = await this.ensureKeypair();
|
|
53
|
+
return keypair.publicKey;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the `Authorization: vapid ...` header value for one delivery to
|
|
57
|
+
* `endpoint`. The JWT's audience is the endpoint's origin (RFC 8292).
|
|
58
|
+
*/
|
|
59
|
+
async buildAuthorizationHeader(endpoint) {
|
|
60
|
+
const keypair = await this.ensureKeypair();
|
|
61
|
+
const audience = new URL(endpoint).origin;
|
|
62
|
+
const jwt = this.signJwt(audience, keypair.privateJwk);
|
|
63
|
+
return `vapid t=${jwt}, k=${keypair.publicKey}`;
|
|
64
|
+
}
|
|
65
|
+
signJwt(audience, privateJwk) {
|
|
66
|
+
const header = base64UrlJson({ typ: 'JWT', alg: 'ES256' });
|
|
67
|
+
const payload = base64UrlJson({
|
|
68
|
+
aud: audience,
|
|
69
|
+
exp: Math.floor(Date.now() / 1000) + JWT_LIFETIME_SECONDS,
|
|
70
|
+
sub: this.subject,
|
|
71
|
+
});
|
|
72
|
+
const signingInput = `${header}.${payload}`;
|
|
73
|
+
const key = createPrivateKey({ key: privateJwk, format: 'jwk' });
|
|
74
|
+
// ieee-p1363 gives the raw r||s (64-byte) JOSE signature ES256 requires,
|
|
75
|
+
// not Node's default DER encoding.
|
|
76
|
+
const signature = cryptoSign('sha256', Buffer.from(signingInput, 'utf8'), {
|
|
77
|
+
key,
|
|
78
|
+
dsaEncoding: 'ieee-p1363',
|
|
79
|
+
});
|
|
80
|
+
return `${signingInput}.${base64UrlFromBuffer(signature)}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Load the keypair from the secrets store, or generate and persist one on
|
|
84
|
+
* first use. Concurrent callers share a single in-flight generation so two
|
|
85
|
+
* simultaneous first-time deliveries cannot mint two keypairs.
|
|
86
|
+
*/
|
|
87
|
+
ensureKeypair() {
|
|
88
|
+
if (this.inflight)
|
|
89
|
+
return this.inflight;
|
|
90
|
+
this.inflight = this.loadOrGenerate().catch((error) => {
|
|
91
|
+
// A failed attempt must not poison every later call.
|
|
92
|
+
this.inflight = null;
|
|
93
|
+
throw error;
|
|
94
|
+
});
|
|
95
|
+
return this.inflight;
|
|
96
|
+
}
|
|
97
|
+
async loadOrGenerate() {
|
|
98
|
+
const existing = await this.store.get(VAPID_SECRET_KEY);
|
|
99
|
+
if (existing) {
|
|
100
|
+
const parsed = JSON.parse(existing);
|
|
101
|
+
if (parsed.publicKey && parsed.privateJwk)
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
105
|
+
const privateJwk = privateKey.export({ format: 'jwk' });
|
|
106
|
+
const keypair = {
|
|
107
|
+
publicKey: publicKeyFromJwk(publicKey.export({ format: 'jwk' })),
|
|
108
|
+
privateJwk,
|
|
109
|
+
};
|
|
110
|
+
await this.store.set(VAPID_SECRET_KEY, JSON.stringify(keypair));
|
|
111
|
+
return keypair;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -343,9 +343,9 @@ export const FEATURE_FLAGS = [
|
|
|
343
343
|
runtimeToggleable: true,
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
|
-
// NOTE: control-plane-gateway is the ONE tier-10 flag that defaults ON (One-Platform
|
|
347
|
-
//
|
|
348
|
-
// leaving this OFF made a fresh daemon return 503 on the live-stream path (the
|
|
346
|
+
// NOTE: control-plane-gateway is the ONE tier-10 flag that defaults ON (One-Platform).
|
|
347
|
+
// A stock daemon (no config) must be able to stream companion chat over SSE;
|
|
348
|
+
// leaving this OFF made a fresh daemon return 503 on the live-stream path (the
|
|
349
349
|
// "stock daemon is dead" bug).
|
|
350
350
|
//
|
|
351
351
|
// HONEST SURFACE STATEMENT: flipping this default ON DOES expose surface — just not
|
|
@@ -5,7 +5,7 @@ import type { ProcessActivity, ProcessNode, ProcessState, ProcessUsage } from '.
|
|
|
5
5
|
export declare function chainNodeId(chainId: string): string;
|
|
6
6
|
/** Subtask node ids are namespaced to avoid colliding with agent/process ids. */
|
|
7
7
|
export declare function subtaskNodeId(subtaskId: string): string;
|
|
8
|
-
/** Work-item node ids are namespaced to avoid colliding with agent/process ids
|
|
8
|
+
/** Work-item node ids are namespaced to avoid colliding with agent/process ids. */
|
|
9
9
|
export declare function workItemNodeId(workItemId: string): string;
|
|
10
10
|
/**
|
|
11
11
|
* One activity side-table entry. Registry-owned; populated from the EXISTING
|
|
@@ -35,7 +35,7 @@ export interface AgentAdapterContext {
|
|
|
35
35
|
readonly chainIds: ReadonlySet<string>;
|
|
36
36
|
/** Raw WrfcSubtask ids present in this snapshot. */
|
|
37
37
|
readonly subtaskIds: ReadonlySet<string>;
|
|
38
|
-
/** Raw orchestration-engine WorkItem ids present in this snapshot
|
|
38
|
+
/** Raw orchestration-engine WorkItem ids present in this snapshot. */
|
|
39
39
|
readonly workItemIds: ReadonlySet<string>;
|
|
40
40
|
/** orchestrationNodeId → owning agentId, for parentNodeId edge resolution. */
|
|
41
41
|
readonly agentIdByOrchestrationNodeId: ReadonlyMap<string, string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../../../src/platform/runtime/fleet/adapters/agent.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,YAAY,EACZ,YAAY,EACb,MAAM,aAAa,CAAC;AAErB,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../../../src/platform/runtime/fleet/adapters/agent.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,YAAY,EACZ,YAAY,EACb,MAAM,aAAa,CAAC;AAErB,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE,UAAU,GAAG,gBAAgB,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;IAC/H,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IAChD,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC3D,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtD,gFAAgF;IAChF,QAAQ,CAAC,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxD,2EAA2E;IAC3E,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzD,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACzC,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,8EAA8E;IAC9E,QAAQ,CAAC,4BAA4B,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnE,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IACtG;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACrC;AAED,wDAAwD;AACxD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,GAAG,SAAS,CAYlF;AA8ED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,mBAAmB,GAAG,WAAW,CA6CrF"}
|