@openchambery/web 1.19.7-beta.5 → 1.19.7-beta.7
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/assets/{AgentAvatar-ziUs4sw3.js → AgentAvatar-Di8F4QJh.js} +1 -1
- package/dist/assets/{AssistantView-neqbgNaR.js → AssistantView-AWe-3bXR.js} +1 -1
- package/dist/assets/{ChatContainer-BBKcNWyJ.js → ChatContainer-XHQSSICA.js} +35 -35
- package/dist/assets/{ContextPanel-C3LRnrhI.js → ContextPanel-BT6bgBJ_.js} +3 -3
- package/dist/assets/{DiagramView-BOuujgEg.js → DiagramView-DNLNZOT6.js} +1 -1
- package/dist/assets/{FadeInOnReveal-BAlsRFJH.js → FadeInOnReveal-CV2s6YN8.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-Cj2Tk6Lb.js → MarkdownRendererImpl-C6E0KL8H.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-BAy5LnnX.js → MobileDetailNavigation-Sg0VMlOz.js} +1 -1
- package/dist/assets/{MobileShareBridge-C03A2Qeb.js → MobileShareBridge-QTUpK3s7.js} +2 -2
- package/dist/assets/{MobileSurface-HlOBVawn.js → MobileSurface-BasV1X13.js} +1 -1
- package/dist/assets/{MultiRunWindow-0gmV5who.js → MultiRunWindow-BxyF4Xy4.js} +1 -1
- package/dist/assets/{OnboardingScreen-CJYyR54B.js → OnboardingScreen-D6rtE7rj.js} +1 -1
- package/dist/assets/{OverlayScrollbar-C-jZ8ITG.js → OverlayScrollbar-DlNZho3q.js} +1 -1
- package/dist/assets/{SettingsView-BaHbjZ-r.js → SettingsView-BywiwJ_a.js} +1 -1
- package/dist/assets/{SettingsWindow-BT-H9i50.js → SettingsWindow-zQ1dc-wl.js} +1 -1
- package/dist/assets/{TerminalView-v4YY-9kA.js → TerminalView-BMXYfo8d.js} +1 -1
- package/dist/assets/{ToolOutputDialog-1bp7aeoj.js → ToolOutputDialog-CFFkJzQ4.js} +1 -1
- package/dist/assets/{appThemeRegistry-ChC97xjW.js → appThemeRegistry-4oEk9SR8.js} +2 -2
- package/dist/assets/{clipboard-Bz0Zm9VJ.js → clipboard-D35XSwID.js} +1 -1
- package/dist/assets/{desktopRelayRestore-LBt0CBOj.js → desktopRelayRestore-8pvI_dSc.js} +1 -1
- package/dist/assets/{dialog-HCE4eLUW.js → dialog-x2hirWBd.js} +1 -1
- package/dist/assets/{es-D9KcV5TX.js → es-C8Sqjsia.js} +1 -1
- package/dist/assets/{fileQueries-Dnn15WsA.js → fileQueries-DGHZqZHc.js} +1 -1
- package/dist/assets/{fr-DPA9qUl3.js → fr-p-Zwm_j2.js} +1 -1
- package/dist/assets/{gitApi-BZQvlhsa.js → gitApi-Dqg8EDsg.js} +1 -1
- package/dist/assets/{insertionBoundaries-CLGkUXKc.js → insertionBoundaries-BtiZxiKR.js} +1 -1
- package/dist/assets/{ja-Dsif3NoD.js → ja-B6REu1qP.js} +1 -1
- package/dist/assets/{ko-Dixx4Ctg.js → ko-CJBNH9IZ.js} +1 -1
- package/dist/assets/{main-BtZPyHBH.js → main-Blsi9j8P.js} +2 -2
- package/dist/assets/{main-W1UkNfir.js → main-D4qq97ip.js} +3 -3
- package/dist/assets/{markdownTheme-TdhQOZSN.js → markdownTheme-SPe4jOwX.js} +1 -1
- package/dist/assets/{miniChat-Dqm16yyZ.js → miniChat-QgI11lL4.js} +2 -2
- package/dist/assets/{mobile-CzSQKW-r.js → mobile-BjMMtvjD.js} +2 -2
- package/dist/assets/{multirun-cDeDA8WZ.js → multirun-C9CKfXyS.js} +1 -1
- package/dist/assets/{persistence-CmjKImz2.js → persistence-w-AiX2i4.js} +1 -1
- package/dist/assets/{pl-Bj78ZGgf.js → pl-B2cBI_7K.js} +1 -1
- package/dist/assets/{platform-skqcMARt.js → platform-4eCmsdol.js} +1 -1
- package/dist/assets/{projectMeta-DJa04ZTk.js → projectMeta-DzGPV_uF.js} +1 -1
- package/dist/assets/{pt-BR-CFL3xSAf.js → pt-BR-CrCc9hmt.js} +1 -1
- package/dist/assets/{radio-CTAfZqAa.js → radio-TX0aocEX.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-CR-zSyTR.js → renderElectronMiniChatApp-DZOBP2No.js} +2 -2
- package/dist/assets/{renderMobileApp-InxP28OZ.js → renderMobileApp-DP1_xIqJ.js} +10 -7
- package/dist/assets/{runtimeConfig-Fsa7BZKx.js → runtimeConfig-ClsErHd9.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-Bp-Y9GZS.js → runtimeEndpointReset-DoLMDfAs.js} +1 -1
- package/dist/assets/{shikiHighlight-kzkGYjXV.js → shikiHighlight-B25fE64n.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Bj8N0VWf.js → sortable-tabs-strip-CNDgyX0B.js} +1 -1
- package/dist/assets/{store-DzpbXWX1.js → store-CL3WP48T.js} +2 -2
- package/dist/assets/{terminalApi-CNqqwjfG.js → terminalApi-CGtOtc8I.js} +1 -1
- package/dist/assets/{uk-BY60Ub3Z.js → uk-C8YKAz0x.js} +1 -1
- package/dist/assets/{useAppFontEffects-KatUA88o.js → useAppFontEffects-D1u63a6o.js} +2 -2
- package/dist/assets/{useAssistantUIStore-DaplPyb0.js → useAssistantUIStore-I0VWrB5V.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-B3Vz7zfM.js → useDesktopWindowControlsLayout-DW_ouBDs.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-CoY7Yf8Y.js → useEffectiveDirectory-D976zCCc.js} +1 -1
- package/dist/assets/{useFontPreferences-D8emhcNv.js → useFontPreferences-BLZ3y6mN.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-90zAmNRR.js → useMobileNavigationStore-lcj5GHR3.js} +4 -4
- package/dist/assets/{useSessionAutoCleanup-B2UfFrMn.js → useSessionAutoCleanup-DMNa_krc.js} +1 -1
- package/dist/assets/useUIStore-CNLmCoPU.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Cf50wR0y.js → useWorkerHighlightedLines-BQNOf2U4.js} +1 -1
- package/dist/assets/{zh-CN-Cox1LaOW.js → zh-CN-CCYwUnmB.js} +1 -1
- package/dist/assets/{zh-TW-B6RU0mxa.js → zh-TW-D8Y6py08.js} +1 -1
- package/dist/index.html +7 -7
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +7 -7
- package/package.json +1 -1
- package/server/lib/notifications/APNS.md +4 -4
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/apns-runtime.js +139 -20
- package/server/lib/notifications/apns-runtime.test.js +66 -7
- package/server/lib/notifications/apns-titles.js +62 -62
- package/server/lib/notifications/apns-titles.test.js +3 -1
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +25 -49
- package/server/lib/opencode/settings-runtime.js +16 -0
- package/dist/assets/useUIStore-B7KJS5G7.js +0 -1
package/dist/index.html
CHANGED
|
@@ -552,26 +552,26 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-Blsi9j8P.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-BkpTTbEi.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/desktop-CfLlmafK.js">
|
|
559
559
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
560
560
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
561
|
-
<link rel="modulepreload" crossorigin href="/assets/useUIStore-
|
|
561
|
+
<link rel="modulepreload" crossorigin href="/assets/useUIStore-CNLmCoPU.js">
|
|
562
562
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
563
563
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
|
|
564
564
|
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
565
|
-
<link rel="modulepreload" crossorigin href="/assets/persistence-
|
|
565
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-w-AiX2i4.js">
|
|
566
566
|
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-C1Ns3DUu.js">
|
|
567
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
567
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-8pvI_dSc.js">
|
|
568
568
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
|
|
569
569
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
|
|
570
|
-
<link rel="modulepreload" crossorigin href="/assets/platform-
|
|
571
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
570
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-4eCmsdol.js">
|
|
571
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CGtOtc8I.js">
|
|
572
572
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
573
573
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
|
|
574
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
574
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-ClsErHd9.js">
|
|
575
575
|
<link rel="stylesheet" crossorigin href="/assets/src-DZt1nWf5.css">
|
|
576
576
|
</head>
|
|
577
577
|
<body class="h-full bg-background text-foreground">
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,26 +74,26 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-QgI11lL4.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-BkpTTbEi.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/desktop-CfLlmafK.js">
|
|
81
81
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
82
82
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
83
|
-
<link rel="modulepreload" crossorigin href="/assets/useUIStore-
|
|
83
|
+
<link rel="modulepreload" crossorigin href="/assets/useUIStore-CNLmCoPU.js">
|
|
84
84
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
85
85
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
|
|
86
86
|
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
87
|
-
<link rel="modulepreload" crossorigin href="/assets/persistence-
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-w-AiX2i4.js">
|
|
88
88
|
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-C1Ns3DUu.js">
|
|
89
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
89
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-8pvI_dSc.js">
|
|
90
90
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
|
|
91
91
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
|
|
92
|
-
<link rel="modulepreload" crossorigin href="/assets/platform-
|
|
93
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-4eCmsdol.js">
|
|
93
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CGtOtc8I.js">
|
|
94
94
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
95
95
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
|
|
96
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
96
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-ClsErHd9.js">
|
|
97
97
|
<link rel="stylesheet" crossorigin href="/assets/src-DZt1nWf5.css">
|
|
98
98
|
</head>
|
|
99
99
|
<body class="h-full bg-background text-foreground">
|
package/dist/mobile.html
CHANGED
|
@@ -63,26 +63,26 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-BjMMtvjD.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-BkpTTbEi.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/desktop-CfLlmafK.js">
|
|
70
70
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
71
71
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
72
|
-
<link rel="modulepreload" crossorigin href="/assets/useUIStore-
|
|
72
|
+
<link rel="modulepreload" crossorigin href="/assets/useUIStore-CNLmCoPU.js">
|
|
73
73
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
74
74
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
|
|
75
75
|
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
76
|
-
<link rel="modulepreload" crossorigin href="/assets/persistence-
|
|
76
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-w-AiX2i4.js">
|
|
77
77
|
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-C1Ns3DUu.js">
|
|
78
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
78
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-8pvI_dSc.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
|
|
81
|
-
<link rel="modulepreload" crossorigin href="/assets/platform-
|
|
82
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-4eCmsdol.js">
|
|
82
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CGtOtc8I.js">
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
84
84
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
|
|
85
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
85
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-ClsErHd9.js">
|
|
86
86
|
<link rel="stylesheet" crossorigin href="/assets/src-DZt1nWf5.css">
|
|
87
87
|
</head>
|
|
88
88
|
<body class="h-full bg-background text-foreground">
|
package/package.json
CHANGED
|
@@ -116,8 +116,8 @@ is server-global, so every device token of a server sees the same badge.
|
|
|
116
116
|
|
|
117
117
|
Host (`apns-runtime.js`):
|
|
118
118
|
- `OPENCHAMBER_PUSH_RELAY_URL` (explicit send-URL override), `OPENCHAMBER_APNS_ENVIRONMENT`
|
|
119
|
-
(`
|
|
120
|
-
auto-generated — nothing to set.
|
|
119
|
+
(`production` default for TestFlight/App Store builds; set `sandbox` for Xcode development
|
|
120
|
+
builds). The signing keypair is auto-generated — nothing to set.
|
|
121
121
|
- Direct fallback: `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8`
|
|
122
122
|
(or `_P8_PATH`), `OPENCHAMBER_APNS_BUNDLE_ID`, `OPENCHAMBER_PUSH_RELAY_DISABLED=true`.
|
|
123
123
|
|
|
@@ -136,8 +136,8 @@ device tokens, or signatures.
|
|
|
136
136
|
2. Give those values only to the Push process (environment or Docker secret). Do not put the
|
|
137
137
|
`.p8` on Layer 1 or on each OpenChamber Host when using Relay mode.
|
|
138
138
|
3. Xcode: confirm the Push Notifications capability; Clean Build Folder; run on device.
|
|
139
|
-
|
|
140
|
-
`OPENCHAMBER_APNS_ENVIRONMENT=
|
|
139
|
+
Hosts default to APNs `production` (TestFlight/App Store tokens). Xcode development
|
|
140
|
+
builds (sandbox tokens) need `OPENCHAMBER_APNS_ENVIRONMENT=sandbox`.
|
|
141
141
|
|
|
142
142
|
## Security posture
|
|
143
143
|
|
|
@@ -79,7 +79,7 @@ This module provides notification message preparation utilities for the web serv
|
|
|
79
79
|
- `sendApnsToAllUiSessions(payload)` — localizes scenario titles per stored token locale (`apns-titles.js`), groups tokens by locale, then signs + sends (no UI-visibility gate; iOS suppresses the foreground banner). Before a relay send, each token is checked against the in-memory registration success cache for the current register URL; cache hits skip `/v1/push/register-token`, misses are registered with bounded concurrency, and only successfully bound tokens are sent. Switching Relay/Push URL clears that cache so the next send re-binds persisted tokens to the new origin. No-ops with a single warning when APNs is unconfigured. Drops tokens on `410` / `BadDeviceToken` / `Unregistered`. Never sends Live Activity tokens.
|
|
80
80
|
- `reRegisterAllTokens()` — binds every persisted device token to the current Push origin (same success cache and URL-switch invalidation as send).
|
|
81
81
|
- `resolveApnsConfig()`
|
|
82
|
-
- Configuration (env first, then `settings.apnsConfig`): `OPENCHAMBER_PUSH_RELAY_URL` (optional send-URL override), `OPENCHAMBER_PUSH_RELAY_DISABLED`, `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8` (PEM contents; literal `\n` accepted) or `OPENCHAMBER_APNS_P8_PATH`, `OPENCHAMBER_APNS_BUNDLE_ID` (default `com.yee94.openchamber`), `OPENCHAMBER_APNS_ENVIRONMENT` (`
|
|
82
|
+
- Configuration (env first, then `settings.apnsConfig`): `OPENCHAMBER_PUSH_RELAY_URL` (optional send-URL override), `OPENCHAMBER_PUSH_RELAY_DISABLED`, `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8` (PEM contents; literal `\n` accepted) or `OPENCHAMBER_APNS_P8_PATH`, `OPENCHAMBER_APNS_BUNDLE_ID` (default `com.yee94.openchamber`), `OPENCHAMBER_APNS_ENVIRONMENT` (`production` default for TestFlight/App Store builds; set `sandbox` explicitly for Xcode development builds).
|
|
83
83
|
|
|
84
84
|
### Emitter runtime API (emitter-runtime.js)
|
|
85
85
|
- `createNotificationEmitterRuntime(dependencies)`: creates runtime for unified notification emission channels.
|
|
@@ -36,6 +36,72 @@ const LIVE_ACTIVITY_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
36
36
|
const LIVE_ACTIVITY_COMPLETE_DISMISSAL_SECONDS = 15 * 60;
|
|
37
37
|
const LIVE_ACTIVITY_ERROR_DISMISSAL_SECONDS = 60 * 60;
|
|
38
38
|
const LIVE_ACTIVITY_STALE_SECONDS = 20 * 60;
|
|
39
|
+
const LIVE_ACTIVITY_AGGREGATE_SESSION_ID = 'live';
|
|
40
|
+
const MAX_LIVE_ACTIVITY_SNAPSHOT_ITEMS = 4;
|
|
41
|
+
const LIVE_ACTIVITY_ITEM_STATUSES = new Set([
|
|
42
|
+
'working', 'tool', 'retry', 'input', 'permission', 'stale', 'complete', 'error',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const parseLiveActivitySnapshot = (value) => {
|
|
46
|
+
if (!Array.isArray(value)) return undefined;
|
|
47
|
+
const items = [];
|
|
48
|
+
for (const raw of value.slice(0, MAX_LIVE_ACTIVITY_SNAPSHOT_ITEMS)) {
|
|
49
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
50
|
+
const sessionId = typeof raw.sessionId === 'string'
|
|
51
|
+
? raw.sessionId.trim()
|
|
52
|
+
: (typeof raw.sessionID === 'string' ? raw.sessionID.trim() : '');
|
|
53
|
+
if (!sessionId || sessionId.length > MAX_LIVE_ACTIVITY_ID_CHARS) continue;
|
|
54
|
+
const status = typeof raw.status === 'string' && LIVE_ACTIVITY_ITEM_STATUSES.has(raw.status)
|
|
55
|
+
? raw.status
|
|
56
|
+
: 'working';
|
|
57
|
+
const startedAt = Number(raw.startedAt);
|
|
58
|
+
if (!Number.isFinite(startedAt)) continue;
|
|
59
|
+
const item = {
|
|
60
|
+
sessionId,
|
|
61
|
+
title: typeof raw.title === 'string' ? raw.title.slice(0, 80) : '',
|
|
62
|
+
status,
|
|
63
|
+
startedAt,
|
|
64
|
+
};
|
|
65
|
+
const endedAt = Number(raw.endedAt);
|
|
66
|
+
if (Number.isFinite(endedAt)) item.endedAt = endedAt;
|
|
67
|
+
items.push(item);
|
|
68
|
+
}
|
|
69
|
+
return items;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const liveActivityItemIsWorking = (status) => status !== 'complete' && status !== 'error';
|
|
73
|
+
|
|
74
|
+
const liveActivityEntryCoversSession = (entry, sessionId) => {
|
|
75
|
+
if (entry.sessionId === sessionId) return true;
|
|
76
|
+
if (Array.isArray(entry.snapshot) && entry.snapshot.length > 0) {
|
|
77
|
+
return entry.snapshot.some((item) => item.sessionId === sessionId);
|
|
78
|
+
}
|
|
79
|
+
return entry.sessionId === LIVE_ACTIVITY_AGGREGATE_SESSION_ID;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const contentStateFromSnapshot = (snapshot, status, eventVersion, updatedAt, endedAt) => {
|
|
83
|
+
const working = snapshot.filter((item) => liveActivityItemIsWorking(item.status));
|
|
84
|
+
const primary = working[0] ?? snapshot[0];
|
|
85
|
+
const contentState = {
|
|
86
|
+
status,
|
|
87
|
+
eventVersion,
|
|
88
|
+
updatedAt,
|
|
89
|
+
workingCount: working.length,
|
|
90
|
+
items: snapshot.map((item) => {
|
|
91
|
+
const next = {
|
|
92
|
+
sessionID: item.sessionId,
|
|
93
|
+
title: item.title,
|
|
94
|
+
status: item.status,
|
|
95
|
+
startedAt: item.startedAt,
|
|
96
|
+
};
|
|
97
|
+
if (item.endedAt !== undefined) next.endedAt = item.endedAt;
|
|
98
|
+
return next;
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
if (primary?.title) contentState.title = primary.title;
|
|
102
|
+
if (endedAt !== undefined) contentState.endedAt = endedAt;
|
|
103
|
+
return contentState;
|
|
104
|
+
};
|
|
39
105
|
const MAX_RELAY_SEND_TOKENS = 100;
|
|
40
106
|
const RELAY_REGISTER_CONCURRENCY = 16;
|
|
41
107
|
const APNS_COLLAPSE_ID_MAX_BYTES = 64;
|
|
@@ -356,7 +422,8 @@ export const createApnsRuntime = (deps) => {
|
|
|
356
422
|
const createdAt = typeof entry.createdAt === 'number' ? entry.createdAt : null;
|
|
357
423
|
const lastSeenAt = typeof entry.lastSeenAt === 'number' ? entry.lastSeenAt : createdAt;
|
|
358
424
|
if (typeof lastSeenAt === 'number' && now - lastSeenAt > LIVE_ACTIVITY_TOKEN_TTL_MS) return null;
|
|
359
|
-
|
|
425
|
+
const snapshot = parseLiveActivitySnapshot(entry.snapshot);
|
|
426
|
+
return { token, activityId, sessionId, snapshot, createdAt, lastSeenAt };
|
|
360
427
|
})
|
|
361
428
|
.filter(Boolean);
|
|
362
429
|
};
|
|
@@ -464,7 +531,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
464
531
|
});
|
|
465
532
|
};
|
|
466
533
|
|
|
467
|
-
const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId) => {
|
|
534
|
+
const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId, snapshot) => {
|
|
468
535
|
if (!uiSessionToken) return;
|
|
469
536
|
const trimmedToken = typeof token === 'string' ? token.trim() : '';
|
|
470
537
|
const trimmedActivityId = typeof activityId === 'string' ? activityId.trim() : '';
|
|
@@ -486,10 +553,12 @@ export const createApnsRuntime = (deps) => {
|
|
|
486
553
|
const filtered = existing.filter(
|
|
487
554
|
(entry) => entry.activityId !== trimmedActivityId && entry.token !== trimmedToken,
|
|
488
555
|
);
|
|
556
|
+
const nextSnapshot = parseLiveActivitySnapshot(snapshot) ?? replaced?.snapshot;
|
|
489
557
|
filtered.unshift({
|
|
490
558
|
token: trimmedToken,
|
|
491
559
|
activityId: trimmedActivityId,
|
|
492
560
|
sessionId: trimmedSessionId,
|
|
561
|
+
snapshot: nextSnapshot,
|
|
493
562
|
createdAt: typeof replaced?.createdAt === 'number' ? replaced.createdAt : now,
|
|
494
563
|
lastSeenAt: now,
|
|
495
564
|
});
|
|
@@ -590,7 +659,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
590
659
|
teamId,
|
|
591
660
|
p8,
|
|
592
661
|
bundleId: bundleId || DEFAULT_BUNDLE_ID,
|
|
593
|
-
environment: environment === 'production' ? 'production' : 'sandbox',
|
|
662
|
+
environment: (environment || 'production') === 'production' ? 'production' : 'sandbox',
|
|
594
663
|
};
|
|
595
664
|
};
|
|
596
665
|
|
|
@@ -710,7 +779,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
710
779
|
liveActivityUnregisterUrl: url.replace(/\/send$/, '/unregister-live-activity-token'),
|
|
711
780
|
liveActivitySendUrl: url.replace(/\/send$/, '/live-activity'),
|
|
712
781
|
environment:
|
|
713
|
-
(trimmedEnv('OPENCHAMBER_APNS_ENVIRONMENT') || '
|
|
782
|
+
(trimmedEnv('OPENCHAMBER_APNS_ENVIRONMENT') || 'production').toLowerCase() === 'production'
|
|
714
783
|
? 'production'
|
|
715
784
|
: 'sandbox',
|
|
716
785
|
});
|
|
@@ -857,9 +926,13 @@ export const createApnsRuntime = (deps) => {
|
|
|
857
926
|
return { dismissalDate: liveActivityDismissalDate(status, nowMs) };
|
|
858
927
|
};
|
|
859
928
|
|
|
860
|
-
const liveActivityRelaySignMessage = ({ ts, tokens, event, contentState, dismissalDate, staleDate }) =>
|
|
861
|
-
`${ts}.${[...tokens].sort().join(',')}.${event}.${contentState.status}.${contentState.eventVersion}.${contentState.updatedAt}.${contentState.endedAt ?? ''}.${dismissalDate ?? ''}.${staleDate ?? ''}
|
|
862
|
-
|
|
929
|
+
const liveActivityRelaySignMessage = ({ ts, tokens, event, contentState, dismissalDate, staleDate }) => {
|
|
930
|
+
let message = `${ts}.${[...tokens].sort().join(',')}.${event}.${contentState.status}.${contentState.eventVersion}.${contentState.updatedAt}.${contentState.endedAt ?? ''}.${dismissalDate ?? ''}.${staleDate ?? ''}`;
|
|
931
|
+
if (contentState.title !== undefined || contentState.workingCount !== undefined || contentState.items !== undefined) {
|
|
932
|
+
message += `.${contentState.title ?? ''}.${contentState.workingCount ?? ''}.${JSON.stringify(contentState.items ?? [])}`;
|
|
933
|
+
}
|
|
934
|
+
return message;
|
|
935
|
+
};
|
|
863
936
|
|
|
864
937
|
const sendLiveActivityViaRelay = async (deviceTokens, payload, relay) => {
|
|
865
938
|
if (!Array.isArray(deviceTokens) || deviceTokens.length === 0) return [];
|
|
@@ -1039,7 +1112,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1039
1112
|
const seen = new Set();
|
|
1040
1113
|
for (const record of Object.values(pruneLiveActivityTokensBySession(store.liveActivityTokensBySession, now))) {
|
|
1041
1114
|
for (const entry of record) {
|
|
1042
|
-
if (entry
|
|
1115
|
+
if (!liveActivityEntryCoversSession(entry, sessionId) || seen.has(entry.token)) continue;
|
|
1043
1116
|
seen.add(entry.token);
|
|
1044
1117
|
entries.push(entry);
|
|
1045
1118
|
}
|
|
@@ -1047,14 +1120,18 @@ export const createApnsRuntime = (deps) => {
|
|
|
1047
1120
|
return entries;
|
|
1048
1121
|
};
|
|
1049
1122
|
|
|
1050
|
-
const sendLiveActivityEnd = async ({ sessionId, status, eventVersion, endedAt } = {}) => {
|
|
1123
|
+
const sendLiveActivityEnd = async ({ sessionId, status, eventVersion, endedAt, title } = {}) => {
|
|
1051
1124
|
const trimmedSessionId = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
1052
1125
|
if (!isLimitedId(trimmedSessionId, MAX_LIVE_ACTIVITY_ID_CHARS)) return;
|
|
1053
|
-
|
|
1126
|
+
const nextStatus = LIVE_ACTIVITY_TERMINAL_STATUSES.has(status) ? status : undefined;
|
|
1127
|
+
const nextTitle = typeof title === 'string' ? title.trim().slice(0, 80) : '';
|
|
1128
|
+
if (!nextStatus && !nextTitle) return;
|
|
1054
1129
|
|
|
1055
1130
|
const now = Date.now();
|
|
1056
1131
|
let pendingEntries = [];
|
|
1057
1132
|
let nextVersion = 0;
|
|
1133
|
+
let event = 'end';
|
|
1134
|
+
let snapshotForSend;
|
|
1058
1135
|
await persistTokenUpdate((current) => {
|
|
1059
1136
|
const liveActivityTokensBySession = pruneLiveActivityTokensBySession(
|
|
1060
1137
|
current.liveActivityTokensBySession,
|
|
@@ -1068,31 +1145,73 @@ export const createApnsRuntime = (deps) => {
|
|
|
1068
1145
|
if (pendingEntries.length === 0) {
|
|
1069
1146
|
return { ...current, liveActivityTokensBySession };
|
|
1070
1147
|
}
|
|
1071
|
-
const previous = current.liveActivityEventVersions?.[trimmedSessionId]
|
|
1148
|
+
const previous = current.liveActivityEventVersions?.[trimmedSessionId]
|
|
1149
|
+
?? current.liveActivityEventVersions?.[LIVE_ACTIVITY_AGGREGATE_SESSION_ID];
|
|
1072
1150
|
nextVersion = nextLiveActivityEventVersion(now, previous, eventVersion);
|
|
1151
|
+
const endedAtSeconds = toLiveActivityUnixSeconds(endedAt, now);
|
|
1152
|
+
for (const [uiSession, record] of Object.entries(liveActivityTokensBySession)) {
|
|
1153
|
+
liveActivityTokensBySession[uiSession] = record.map((entry) => {
|
|
1154
|
+
if (!liveActivityEntryCoversSession(entry, trimmedSessionId)) return entry;
|
|
1155
|
+
if (!Array.isArray(entry.snapshot) || entry.snapshot.length === 0) return entry;
|
|
1156
|
+
const snapshot = entry.snapshot.map((item) => {
|
|
1157
|
+
if (item.sessionId !== trimmedSessionId) return item;
|
|
1158
|
+
const patched = { ...item };
|
|
1159
|
+
if (nextTitle) patched.title = nextTitle;
|
|
1160
|
+
if (nextStatus) {
|
|
1161
|
+
patched.status = nextStatus;
|
|
1162
|
+
patched.endedAt = endedAtSeconds;
|
|
1163
|
+
}
|
|
1164
|
+
return patched;
|
|
1165
|
+
});
|
|
1166
|
+
snapshotForSend = snapshot;
|
|
1167
|
+
return { ...entry, snapshot };
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
const working = Array.isArray(snapshotForSend)
|
|
1171
|
+
? snapshotForSend.filter((item) => liveActivityItemIsWorking(item.status))
|
|
1172
|
+
: [];
|
|
1173
|
+
event = nextStatus && working.length === 0 ? 'end' : 'update';
|
|
1174
|
+
if (!nextStatus) event = 'update';
|
|
1073
1175
|
return {
|
|
1074
1176
|
...current,
|
|
1075
1177
|
liveActivityTokensBySession,
|
|
1076
1178
|
liveActivityEventVersions: {
|
|
1077
1179
|
...(current.liveActivityEventVersions || {}),
|
|
1078
1180
|
[trimmedSessionId]: nextVersion,
|
|
1181
|
+
[LIVE_ACTIVITY_AGGREGATE_SESSION_ID]: nextVersion,
|
|
1079
1182
|
},
|
|
1080
1183
|
};
|
|
1081
1184
|
});
|
|
1082
1185
|
if (pendingEntries.length === 0) return;
|
|
1186
|
+
if (!nextStatus && !(Array.isArray(snapshotForSend) && snapshotForSend.length > 0)) return;
|
|
1083
1187
|
|
|
1084
1188
|
const updatedAt = now / 1000;
|
|
1085
1189
|
const endedAtSeconds = toLiveActivityUnixSeconds(endedAt, now);
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1190
|
+
const aggregateStatus = event === 'end'
|
|
1191
|
+
? (nextStatus || 'complete')
|
|
1192
|
+
: (Array.isArray(snapshotForSend)
|
|
1193
|
+
? (snapshotForSend.some((item) => liveActivityItemIsWorking(item.status))
|
|
1194
|
+
? (snapshotForSend.find((item) => liveActivityItemIsWorking(item.status))?.status || 'working')
|
|
1195
|
+
: (nextStatus || 'working'))
|
|
1196
|
+
: (nextStatus || 'working'));
|
|
1197
|
+
const contentState = Array.isArray(snapshotForSend) && snapshotForSend.length > 0
|
|
1198
|
+
? contentStateFromSnapshot(
|
|
1199
|
+
snapshotForSend,
|
|
1200
|
+
aggregateStatus,
|
|
1201
|
+
nextVersion,
|
|
1202
|
+
updatedAt,
|
|
1203
|
+
event === 'end' ? endedAtSeconds : undefined,
|
|
1204
|
+
)
|
|
1205
|
+
: {
|
|
1206
|
+
status: nextStatus || 'complete',
|
|
1207
|
+
eventVersion: nextVersion,
|
|
1208
|
+
updatedAt,
|
|
1209
|
+
...(event === 'end' ? { endedAt: endedAtSeconds } : {}),
|
|
1210
|
+
};
|
|
1092
1211
|
const payload = {
|
|
1093
|
-
event
|
|
1212
|
+
event,
|
|
1094
1213
|
contentState,
|
|
1095
|
-
...liveActivityPushDates(
|
|
1214
|
+
...liveActivityPushDates(event, contentState.status, updatedAt, now),
|
|
1096
1215
|
};
|
|
1097
1216
|
|
|
1098
1217
|
const tokens = pendingEntries.map((entry) => entry.token);
|
|
@@ -1121,7 +1240,7 @@ export const createApnsRuntime = (deps) => {
|
|
|
1121
1240
|
return;
|
|
1122
1241
|
}
|
|
1123
1242
|
|
|
1124
|
-
if (accepted.length > 0) {
|
|
1243
|
+
if (event === 'end' && accepted.length > 0) {
|
|
1125
1244
|
await removeLiveActivityTokens(accepted);
|
|
1126
1245
|
}
|
|
1127
1246
|
};
|
|
@@ -115,6 +115,25 @@ describe('apns runtime bundle ID', () => {
|
|
|
115
115
|
});
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
describe('apns runtime APNs environment', () => {
|
|
119
|
+
it('defaults resolveApnsConfig environment to production when env and settings omit it', async () => {
|
|
120
|
+
process.env.OPENCHAMBER_APNS_KEY_ID = 'KEY123';
|
|
121
|
+
process.env.OPENCHAMBER_APNS_TEAM_ID = 'TEAM123';
|
|
122
|
+
process.env.OPENCHAMBER_APNS_P8 = P8;
|
|
123
|
+
const runtime = createApnsRuntime(makeDeps());
|
|
124
|
+
await expect(runtime.resolveApnsConfig()).resolves.toMatchObject({ environment: 'production' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('uses sandbox when OPENCHAMBER_APNS_ENVIRONMENT=sandbox', async () => {
|
|
128
|
+
process.env.OPENCHAMBER_APNS_KEY_ID = 'KEY123';
|
|
129
|
+
process.env.OPENCHAMBER_APNS_TEAM_ID = 'TEAM123';
|
|
130
|
+
process.env.OPENCHAMBER_APNS_P8 = P8;
|
|
131
|
+
process.env.OPENCHAMBER_APNS_ENVIRONMENT = 'sandbox';
|
|
132
|
+
const runtime = createApnsRuntime(makeDeps());
|
|
133
|
+
await expect(runtime.resolveApnsConfig()).resolves.toMatchObject({ environment: 'sandbox' });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
118
137
|
describe('apns runtime relay mode (default)', () => {
|
|
119
138
|
it('registers tokens (signed) and posts signed generic text, dropping dead tokens', async () => {
|
|
120
139
|
const fetchMock = vi.fn(async (url) =>
|
|
@@ -618,9 +637,9 @@ describe('apns runtime push relay derivation and re-register', () => {
|
|
|
618
637
|
expect(sendBodies).toHaveLength(3);
|
|
619
638
|
|
|
620
639
|
const byTitle = Object.fromEntries(sendBodies.map((body) => [body.title, body]));
|
|
621
|
-
expect(byTitle['
|
|
622
|
-
expect(byTitle['
|
|
623
|
-
expect(byTitle['
|
|
640
|
+
expect(byTitle['Task completed']?.tokens).toEqual(['tokenEn']);
|
|
641
|
+
expect(byTitle['任务已完成']?.tokens).toEqual(['tokenZh']);
|
|
642
|
+
expect(byTitle['タスク完了']?.tokens).toEqual(['tokenJa']);
|
|
624
643
|
for (const body of sendBodies) {
|
|
625
644
|
expect(body.body).toBe('My session');
|
|
626
645
|
expect(body.badge).toBe(2);
|
|
@@ -641,7 +660,7 @@ describe('apns runtime push relay derivation and re-register', () => {
|
|
|
641
660
|
await runtime.sendApnsToAllUiSessions({ type: 'permission', sessionName: 'S' });
|
|
642
661
|
|
|
643
662
|
const sent = JSON.parse(fetchMock.mock.calls.find(isSend)[1].body);
|
|
644
|
-
expect(sent.title).toBe('
|
|
663
|
+
expect(sent.title).toBe('Needs permission');
|
|
645
664
|
expect(sent.body).toBe('S');
|
|
646
665
|
});
|
|
647
666
|
});
|
|
@@ -840,7 +859,7 @@ describe('apns live activity routes', () => {
|
|
|
840
859
|
body: { token: ' la-token ', activityId: ' act-1 ', sessionId: ' ses_1 ' },
|
|
841
860
|
}, postRes);
|
|
842
861
|
expect(postRes.body).toEqual({ ok: true });
|
|
843
|
-
expect(addOrUpdateLiveActivityToken).toHaveBeenCalledWith('ui-token', 'la-token', 'act-1', 'ses_1');
|
|
862
|
+
expect(addOrUpdateLiveActivityToken).toHaveBeenCalledWith('ui-token', 'la-token', 'act-1', 'ses_1', undefined);
|
|
844
863
|
|
|
845
864
|
const delRes = createMockRes();
|
|
846
865
|
await getRoute('DELETE', '/api/push/live-activity-token')({ body: { token: 'la-token' } }, delRes);
|
|
@@ -906,6 +925,34 @@ describe('apns live activity relay and direct delivery', () => {
|
|
|
906
925
|
}
|
|
907
926
|
});
|
|
908
927
|
|
|
928
|
+
it('updates the aggregate live activity when one covered session completes', async () => {
|
|
929
|
+
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
|
|
930
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
931
|
+
process.env.OPENCHAMBER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
|
|
932
|
+
const runtime = createApnsRuntime(makeDeps());
|
|
933
|
+
const token = 'a'.repeat(64);
|
|
934
|
+
await runtime.addOrUpdateLiveActivityToken('s1', token, 'act-1', 'live', [
|
|
935
|
+
{ sessionId: 'ses_a', title: 'Alpha', status: 'working', startedAt: 100 },
|
|
936
|
+
{ sessionId: 'ses_b', title: 'Beta', status: 'working', startedAt: 100 },
|
|
937
|
+
]);
|
|
938
|
+
fetchMock.mockClear();
|
|
939
|
+
await runtime.sendLiveActivityEnd({ sessionId: 'ses_a', status: 'complete' });
|
|
940
|
+
const sent = JSON.parse(fetchMock.mock.calls.find(isLiveActivitySend)[1].body);
|
|
941
|
+
expect(sent.event).toBe('update');
|
|
942
|
+
expect(sent.contentState.workingCount).toBe(1);
|
|
943
|
+
expect(sent.contentState.items).toEqual([
|
|
944
|
+
expect.objectContaining({ sessionID: 'ses_a', status: 'complete', title: 'Alpha' }),
|
|
945
|
+
expect.objectContaining({ sessionID: 'ses_b', status: 'working', title: 'Beta' }),
|
|
946
|
+
]);
|
|
947
|
+
expect(sent.staleDate).toBeTypeOf('number');
|
|
948
|
+
expect(sent.dismissalDate).toBeUndefined();
|
|
949
|
+
await verifyRelaySignature(
|
|
950
|
+
sent.publicKeyJwk,
|
|
951
|
+
`${sent.ts}.${[...sent.tokens].sort().join(',')}.${sent.event}.${sent.contentState.status}.${sent.contentState.eventVersion}.${sent.contentState.updatedAt}.${sent.contentState.endedAt ?? ''}.${sent.dismissalDate ?? ''}.${sent.staleDate ?? ''}.${sent.contentState.title ?? ''}.${sent.contentState.workingCount ?? ''}.${JSON.stringify(sent.contentState.items ?? [])}`,
|
|
952
|
+
sent.sig,
|
|
953
|
+
).then((ok) => expect(ok).toBe(true));
|
|
954
|
+
});
|
|
955
|
+
|
|
909
956
|
it('uses a 60-minute dismissal for error ends', async () => {
|
|
910
957
|
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
|
|
911
958
|
vi.stubGlobal('fetch', fetchMock);
|
|
@@ -921,11 +968,10 @@ describe('apns live activity relay and direct delivery', () => {
|
|
|
921
968
|
expect(sent.dismissalDate).toBeLessThanOrEqual(Math.floor(after / 1000) + 60 * 60);
|
|
922
969
|
});
|
|
923
970
|
|
|
924
|
-
it('
|
|
971
|
+
it('defaults live-activity relay env to production when OPENCHAMBER_APNS_ENVIRONMENT is unset', async () => {
|
|
925
972
|
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
|
|
926
973
|
vi.stubGlobal('fetch', fetchMock);
|
|
927
974
|
process.env.OPENCHAMBER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
|
|
928
|
-
process.env.OPENCHAMBER_APNS_ENVIRONMENT = 'production';
|
|
929
975
|
const runtime = createApnsRuntime(makeDeps());
|
|
930
976
|
await runtime.addOrUpdateLiveActivityToken('s1', 'la-token', 'act-1', 'ses_1');
|
|
931
977
|
fetchMock.mockClear();
|
|
@@ -934,6 +980,19 @@ describe('apns live activity relay and direct delivery', () => {
|
|
|
934
980
|
expect(sent.env).toBe('production');
|
|
935
981
|
});
|
|
936
982
|
|
|
983
|
+
it('forwards OPENCHAMBER_APNS_ENVIRONMENT=sandbox as env on live-activity relay send', async () => {
|
|
984
|
+
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
|
|
985
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
986
|
+
process.env.OPENCHAMBER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
|
|
987
|
+
process.env.OPENCHAMBER_APNS_ENVIRONMENT = 'sandbox';
|
|
988
|
+
const runtime = createApnsRuntime(makeDeps());
|
|
989
|
+
await runtime.addOrUpdateLiveActivityToken('s1', 'la-token', 'act-1', 'ses_1');
|
|
990
|
+
fetchMock.mockClear();
|
|
991
|
+
await runtime.sendLiveActivityEnd({ sessionId: 'ses_1', status: 'complete' });
|
|
992
|
+
const sent = JSON.parse(fetchMock.mock.calls.find(isLiveActivitySend)[1].body);
|
|
993
|
+
expect(sent.env).toBe('sandbox');
|
|
994
|
+
});
|
|
995
|
+
|
|
937
996
|
it('keeps tokens after a failed relay end so a later attempt can retry', async () => {
|
|
938
997
|
const fetchMock = vi.fn(async (url) => {
|
|
939
998
|
if (String(url).endsWith('/live-activity')) return jsonResponse({ error: true }, 500);
|