@serfab/cadre-core 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +9 -8
  2. package/dist/authority-key.d.ts +41 -0
  3. package/dist/authority-key.js +52 -0
  4. package/dist/authority-key.js.map +1 -0
  5. package/dist/cadre-node.d.ts +770 -11
  6. package/dist/cadre-node.js +1900 -88
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/canonical-datetime.d.ts +33 -0
  9. package/dist/canonical-datetime.js +38 -0
  10. package/dist/canonical-datetime.js.map +1 -0
  11. package/dist/canonical-json.d.ts +12 -0
  12. package/dist/canonical-json.js +33 -0
  13. package/dist/canonical-json.js.map +1 -0
  14. package/dist/control-cohort.d.ts +63 -0
  15. package/dist/control-cohort.js +80 -0
  16. package/dist/control-cohort.js.map +1 -0
  17. package/dist/control-database.d.ts +259 -5
  18. package/dist/control-database.js +541 -128
  19. package/dist/control-database.js.map +1 -1
  20. package/dist/control-formation-recorder.d.ts +89 -0
  21. package/dist/control-formation-recorder.js +139 -0
  22. package/dist/control-formation-recorder.js.map +1 -0
  23. package/dist/control-schema.d.ts +12 -0
  24. package/dist/control-schema.js +199 -0
  25. package/dist/control-schema.js.map +1 -0
  26. package/dist/control-stream.d.ts +54 -0
  27. package/dist/control-stream.js +104 -0
  28. package/dist/control-stream.js.map +1 -0
  29. package/dist/device-token.d.ts +50 -0
  30. package/dist/device-token.js +66 -0
  31. package/dist/device-token.js.map +1 -0
  32. package/dist/diagnostics/connection-path.d.ts +118 -0
  33. package/dist/diagnostics/connection-path.js +204 -0
  34. package/dist/diagnostics/connection-path.js.map +1 -0
  35. package/dist/diagnostics/webrtc-turn-tracker.d.ts +50 -0
  36. package/dist/diagnostics/webrtc-turn-tracker.js +196 -0
  37. package/dist/diagnostics/webrtc-turn-tracker.js.map +1 -0
  38. package/dist/hibernation-manager.d.ts +79 -0
  39. package/dist/hibernation-manager.js +173 -23
  40. package/dist/hibernation-manager.js.map +1 -1
  41. package/dist/index.d.ts +25 -4
  42. package/dist/index.js +59 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/key-store-file.d.ts +52 -0
  45. package/dist/key-store-file.js +194 -0
  46. package/dist/key-store-file.js.map +1 -0
  47. package/dist/key-store.d.ts +75 -0
  48. package/dist/key-store.js +66 -0
  49. package/dist/key-store.js.map +1 -0
  50. package/dist/peer-authorization.d.ts +26 -0
  51. package/dist/peer-authorization.js +40 -0
  52. package/dist/peer-authorization.js.map +1 -0
  53. package/dist/peer-record.d.ts +89 -0
  54. package/dist/peer-record.js +117 -0
  55. package/dist/peer-record.js.map +1 -0
  56. package/dist/push-credentials.d.ts +34 -0
  57. package/dist/push-credentials.js +81 -0
  58. package/dist/push-credentials.js.map +1 -0
  59. package/dist/push-fanout.d.ts +137 -0
  60. package/dist/push-fanout.js +197 -0
  61. package/dist/push-fanout.js.map +1 -0
  62. package/dist/push-notifier-apns.d.ts +53 -0
  63. package/dist/push-notifier-apns.js +195 -0
  64. package/dist/push-notifier-apns.js.map +1 -0
  65. package/dist/push-notifier-fcm.d.ts +38 -0
  66. package/dist/push-notifier-fcm.js +183 -0
  67. package/dist/push-notifier-fcm.js.map +1 -0
  68. package/dist/push-notifier-shared.d.ts +18 -0
  69. package/dist/push-notifier-shared.js +25 -0
  70. package/dist/push-notifier-shared.js.map +1 -0
  71. package/dist/push-notifier.d.ts +71 -0
  72. package/dist/push-notifier.js +50 -0
  73. package/dist/push-notifier.js.map +1 -0
  74. package/dist/schema-verification.d.ts +11 -2
  75. package/dist/schema-verification.js +15 -4
  76. package/dist/schema-verification.js.map +1 -1
  77. package/dist/seed-bootstrap.d.ts +197 -9
  78. package/dist/seed-bootstrap.js +396 -137
  79. package/dist/seed-bootstrap.js.map +1 -1
  80. package/dist/seed-trust-policy.d.ts +63 -0
  81. package/dist/seed-trust-policy.js +56 -0
  82. package/dist/seed-trust-policy.js.map +1 -0
  83. package/dist/strand-addr-protocol.d.ts +132 -0
  84. package/dist/strand-addr-protocol.js +313 -0
  85. package/dist/strand-addr-protocol.js.map +1 -0
  86. package/dist/strand-cohort.d.ts +54 -0
  87. package/dist/strand-cohort.js +38 -0
  88. package/dist/strand-cohort.js.map +1 -0
  89. package/dist/strand-database.d.ts +54 -6
  90. package/dist/strand-database.js +88 -100
  91. package/dist/strand-database.js.map +1 -1
  92. package/dist/strand-formation-manager.d.ts +79 -19
  93. package/dist/strand-formation-manager.js +196 -104
  94. package/dist/strand-formation-manager.js.map +1 -1
  95. package/dist/strand-formation-protocol.d.ts +162 -0
  96. package/dist/strand-formation-protocol.js +279 -0
  97. package/dist/strand-formation-protocol.js.map +1 -0
  98. package/dist/strand-instance-manager.d.ts +73 -4
  99. package/dist/strand-instance-manager.js +167 -41
  100. package/dist/strand-instance-manager.js.map +1 -1
  101. package/dist/strand-member-key.d.ts +29 -0
  102. package/dist/strand-member-key.js +38 -0
  103. package/dist/strand-member-key.js.map +1 -0
  104. package/dist/strand-member-registry.d.ts +98 -0
  105. package/dist/strand-member-registry.js +129 -0
  106. package/dist/strand-member-registry.js.map +1 -0
  107. package/dist/strand-membership-writer.d.ts +361 -0
  108. package/dist/strand-membership-writer.js +449 -0
  109. package/dist/strand-membership-writer.js.map +1 -0
  110. package/dist/strand-solicitation.d.ts +77 -2
  111. package/dist/strand-solicitation.js +25 -4
  112. package/dist/strand-solicitation.js.map +1 -1
  113. package/dist/strand-wake-payload.d.ts +30 -0
  114. package/dist/strand-wake-payload.js +20 -0
  115. package/dist/strand-wake-payload.js.map +1 -0
  116. package/dist/strand-wake-protocol.d.ts +114 -0
  117. package/dist/strand-wake-protocol.js +257 -0
  118. package/dist/strand-wake-protocol.js.map +1 -0
  119. package/dist/strand-watcher.d.ts +6 -2
  120. package/dist/strand-watcher.js +57 -17
  121. package/dist/strand-watcher.js.map +1 -1
  122. package/dist/types.d.ts +406 -23
  123. package/dist/types.js +22 -5
  124. package/dist/types.js.map +1 -1
  125. package/package.json +14 -8
  126. package/src/authority-key.ts +68 -0
  127. package/src/cadre-node.ts +2937 -948
  128. package/src/canonical-datetime.ts +39 -0
  129. package/src/canonical-json.ts +30 -0
  130. package/src/control-cohort.ts +103 -0
  131. package/src/control-database.ts +931 -383
  132. package/src/control-formation-recorder.ts +151 -0
  133. package/src/control-schema.ts +198 -0
  134. package/src/control-stream.ts +128 -0
  135. package/src/device-token.ts +89 -0
  136. package/src/diagnostics/connection-path.ts +277 -0
  137. package/src/diagnostics/webrtc-turn-tracker.ts +222 -0
  138. package/src/hibernation-manager.ts +197 -28
  139. package/src/index.ts +273 -79
  140. package/src/key-store-file.ts +203 -0
  141. package/src/key-store.ts +99 -0
  142. package/src/peer-authorization.ts +53 -0
  143. package/src/peer-record.ts +146 -0
  144. package/src/push-credentials.ts +84 -0
  145. package/src/push-fanout.ts +272 -0
  146. package/src/push-notifier-apns.ts +243 -0
  147. package/src/push-notifier-fcm.ts +221 -0
  148. package/src/push-notifier-shared.ts +28 -0
  149. package/src/push-notifier.ts +93 -0
  150. package/src/schema-verification.ts +18 -4
  151. package/src/seed-bootstrap.ts +464 -188
  152. package/src/seed-trust-policy.ts +107 -0
  153. package/src/strand-addr-protocol.ts +410 -0
  154. package/src/strand-cohort.ts +77 -0
  155. package/src/strand-database.ts +113 -141
  156. package/src/strand-formation-manager.ts +233 -142
  157. package/src/strand-formation-protocol.ts +391 -0
  158. package/src/strand-instance-manager.ts +207 -56
  159. package/src/strand-member-key.ts +39 -0
  160. package/src/strand-member-registry.ts +177 -0
  161. package/src/strand-membership-writer.ts +640 -0
  162. package/src/strand-solicitation.ts +93 -4
  163. package/src/strand-wake-payload.ts +32 -0
  164. package/src/strand-wake-protocol.ts +331 -0
  165. package/src/strand-watcher.ts +61 -17
  166. package/src/types.ts +1012 -582
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-fanout.js","sourceRoot":"","sources":["../src/push-fanout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * push-notifier-apns.ts — APNs HTTP/2 strand-wake delivery.
3
+ *
4
+ * Server-only: signs a provider JWT (ES256, JOSE raw r‖s via `node:crypto`) and
5
+ * sends a background data push over an HTTP/2 session to Apple's gateway.
6
+ *
7
+ * The HTTP/2 call is behind an injected `Http2Requester` seam (bundled with a
8
+ * `close` into an {@link ApnsTransport}) so unit tests assert the request shape
9
+ * and map every documented response code with no real network and no credentials.
10
+ * The default transport owns a single lazily-(re)established `node:http2` session.
11
+ *
12
+ * Two single-shot retries guard transient failure without a retry storm: a
13
+ * thrown transport error (session death / GOAWAY mid-send) re-establishes the
14
+ * session and retries the one request once; a 403 `ExpiredProviderToken`
15
+ * re-mints the provider JWT and retries once. A second failure is returned as-is.
16
+ *
17
+ * Secret hygiene: the `.p8` `privateKey`, the minted JWT, and full device tokens
18
+ * are never logged — failure log lines carry only a status/reason and a redacted
19
+ * token prefix.
20
+ */
21
+ import type { ApnsCredentials } from './types.js';
22
+ import type { PushNotifier } from './push-notifier.js';
23
+ /** Raw HTTP/2 response: APNs `:status` and the (possibly empty) JSON body. */
24
+ export interface ApnsResponse {
25
+ status: number;
26
+ body: string;
27
+ }
28
+ /** A single APNs HTTP/2 request — `:path`, headers, and the JSON body. */
29
+ export interface ApnsRequest {
30
+ /** `:path`, e.g. `/3/device/{token}`. */
31
+ path: string;
32
+ headers: Record<string, string>;
33
+ body: string;
34
+ }
35
+ /**
36
+ * Low-level HTTP/2 request seam. Resolves with the `(status, body)` pair; rejects
37
+ * (throws) on a session/transport failure so the caller can re-establish + retry.
38
+ */
39
+ export type Http2Requester = (req: ApnsRequest) => Promise<ApnsResponse>;
40
+ /** The HTTP/2 request seam plus its session-teardown. */
41
+ export interface ApnsTransport {
42
+ request: Http2Requester;
43
+ close(): Promise<void>;
44
+ }
45
+ export interface ApnsPushDeps {
46
+ /** HTTP/2 transport. Defaults to a real `node:http2` session manager. */
47
+ transport?: ApnsTransport;
48
+ /** Monotonic clock (ms). Defaults to `Date.now`. Injected for token-cache tests. */
49
+ now?: () => number;
50
+ /** Failure log seam (default: `debug('sereus:cadre:push:apns')`). */
51
+ log?: (line: string) => void;
52
+ }
53
+ export declare function createApnsPushNotifier(creds: ApnsCredentials, deps?: ApnsPushDeps): PushNotifier;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * push-notifier-apns.ts — APNs HTTP/2 strand-wake delivery.
3
+ *
4
+ * Server-only: signs a provider JWT (ES256, JOSE raw r‖s via `node:crypto`) and
5
+ * sends a background data push over an HTTP/2 session to Apple's gateway.
6
+ *
7
+ * The HTTP/2 call is behind an injected `Http2Requester` seam (bundled with a
8
+ * `close` into an {@link ApnsTransport}) so unit tests assert the request shape
9
+ * and map every documented response code with no real network and no credentials.
10
+ * The default transport owns a single lazily-(re)established `node:http2` session.
11
+ *
12
+ * Two single-shot retries guard transient failure without a retry storm: a
13
+ * thrown transport error (session death / GOAWAY mid-send) re-establishes the
14
+ * session and retries the one request once; a 403 `ExpiredProviderToken`
15
+ * re-mints the provider JWT and retries once. A second failure is returned as-is.
16
+ *
17
+ * Secret hygiene: the `.p8` `privateKey`, the minted JWT, and full device tokens
18
+ * are never logged — failure log lines carry only a status/reason and a redacted
19
+ * token prefix.
20
+ */
21
+ // Namespace import (not `import { sign }`): a *named* import of the
22
+ // browser-externalized `node:crypto` hard-fails the web build, whereas a
23
+ // namespace import only warns — same reasoning as the `node:http2` import below
24
+ // and the matching note in push-notifier-fcm.ts. Server-only; never run in browser.
25
+ import * as nodeCrypto from 'node:crypto';
26
+ import * as http2 from 'node:http2';
27
+ import debug from 'debug';
28
+ import { MAX_REASON_LEN, b64urlJson, errText, redact } from './push-notifier-shared.js';
29
+ const APNS_PROD_HOST = 'https://api.push.apple.com';
30
+ const APNS_SANDBOX_HOST = 'https://api.sandbox.push.apple.com';
31
+ /** Apple requires the provider token be 20–60 min old; refresh well inside that. */
32
+ const PROVIDER_TOKEN_TTL_MS = 45 * 60000;
33
+ const debugApns = debug('sereus:cadre:push:apns');
34
+ export function createApnsPushNotifier(creds, deps = {}) {
35
+ const host = creds.production ? APNS_PROD_HOST : APNS_SANDBOX_HOST;
36
+ const transport = deps.transport ?? createHttp2Transport(host);
37
+ const now = deps.now ?? (() => Date.now());
38
+ const log = deps.log ?? ((line) => debugApns(line));
39
+ let cached = null;
40
+ async function send(msg) {
41
+ let jwt;
42
+ try {
43
+ jwt = providerToken(false);
44
+ }
45
+ catch (err) {
46
+ log(`provider-token mint failed: ${errText(err)}`);
47
+ return { ok: false, unregistered: false, error: `apns jwt mint failed: ${errText(err)}` };
48
+ }
49
+ let res = await tryRequest(msg, jwt);
50
+ if (!res)
51
+ return { ok: false, unregistered: false, error: `apns transport failed (token=${redact(msg.token)})` };
52
+ // Provider token expired mid-flight — re-mint once and retry the single send.
53
+ if (res.status === 403 && /ExpiredProviderToken/i.test(res.body)) {
54
+ try {
55
+ jwt = providerToken(true);
56
+ }
57
+ catch (err) {
58
+ log(`provider-token re-mint failed: ${errText(err)}`);
59
+ return { ok: false, unregistered: false, error: `apns jwt re-mint failed: ${errText(err)}` };
60
+ }
61
+ res = await tryRequest(msg, jwt);
62
+ if (!res)
63
+ return { ok: false, unregistered: false, error: `apns transport failed (token=${redact(msg.token)})` };
64
+ }
65
+ return mapResult(res, msg.token);
66
+ }
67
+ /**
68
+ * Send one request with a single re-establish-on-throw retry. A thrown error is
69
+ * a session death / GOAWAY; the default transport recreates its session lazily
70
+ * on the next call, so a second attempt rides a fresh session. Returns `null`
71
+ * when both attempts throw.
72
+ */
73
+ async function tryRequest(msg, jwt) {
74
+ const req = buildRequest(msg, jwt);
75
+ try {
76
+ return await transport.request(req);
77
+ }
78
+ catch (err1) {
79
+ log(`http2 request failed, re-establishing once: ${errText(err1)}`);
80
+ try {
81
+ return await transport.request(req);
82
+ }
83
+ catch (err2) {
84
+ log(`http2 request failed after re-establish: ${errText(err2)}`);
85
+ return null;
86
+ }
87
+ }
88
+ }
89
+ function mapResult(res, deviceToken) {
90
+ if (res.status === 200)
91
+ return { ok: true };
92
+ const reason = apnsReason(res.body);
93
+ const summary = `apns ${res.status}${reason ? ` ${reason}` : ''}`;
94
+ if (res.status === 410 || reason === 'Unregistered' || (res.status === 400 && reason === 'BadDeviceToken')) {
95
+ log(`unregistered token=${redact(deviceToken)} (${summary})`);
96
+ return { ok: false, unregistered: true, error: summary };
97
+ }
98
+ log(`send failed token=${redact(deviceToken)} (${summary})`);
99
+ return { ok: false, unregistered: false, error: summary };
100
+ }
101
+ function buildRequest(msg, jwt) {
102
+ const body = JSON.stringify({
103
+ aps: { 'content-available': 1 },
104
+ type: msg.payload.type,
105
+ strandId: msg.payload.strandId,
106
+ reason: msg.payload.reason.slice(0, MAX_REASON_LEN),
107
+ });
108
+ return {
109
+ path: `/3/device/${msg.token}`,
110
+ headers: {
111
+ authorization: `bearer ${jwt}`,
112
+ 'apns-topic': creds.bundleId,
113
+ 'apns-push-type': 'background',
114
+ 'apns-priority': '5',
115
+ 'apns-expiration': '0',
116
+ },
117
+ body,
118
+ };
119
+ }
120
+ function providerToken(forceRefresh) {
121
+ if (!forceRefresh && cached && cached.expiresAt > now())
122
+ return cached.value;
123
+ const iat = Math.floor(now() / 1000);
124
+ const header = { alg: 'ES256', kid: creds.keyId };
125
+ const claims = { iss: creds.teamId, iat };
126
+ const signingInput = `${b64urlJson(header)}.${b64urlJson(claims)}`;
127
+ const sig = nodeCrypto.sign('SHA256', Buffer.from(signingInput), {
128
+ key: creds.privateKey,
129
+ dsaEncoding: 'ieee-p1363',
130
+ }).toString('base64url');
131
+ const jwt = `${signingInput}.${sig}`;
132
+ cached = { value: jwt, expiresAt: now() + PROVIDER_TOKEN_TTL_MS };
133
+ return jwt;
134
+ }
135
+ async function close() {
136
+ cached = null;
137
+ await transport.close();
138
+ }
139
+ return { send, close };
140
+ }
141
+ /**
142
+ * Default {@link ApnsTransport}: a single lazily-(re)established `node:http2`
143
+ * session to Apple's gateway. A session that errors / receives GOAWAY / closes is
144
+ * dropped so the next request re-establishes it. Not exercised by unit tests (no
145
+ * network) — tests inject a fake transport; the real path is validated out-of-band.
146
+ */
147
+ function createHttp2Transport(host) {
148
+ let session = null;
149
+ function ensureSession() {
150
+ if (session && !session.closed && !session.destroyed)
151
+ return session;
152
+ const s = http2.connect(host);
153
+ // Drop a dead session so ensureSession recreates it on the next request.
154
+ s.on('error', () => { if (session === s)
155
+ session = null; });
156
+ s.on('goaway', () => { s.destroy(); });
157
+ s.on('close', () => { if (session === s)
158
+ session = null; });
159
+ session = s;
160
+ return s;
161
+ }
162
+ async function request(req) {
163
+ const s = ensureSession();
164
+ return await new Promise((resolve, reject) => {
165
+ const stream = s.request({ ':method': 'POST', ':path': req.path, ...req.headers });
166
+ const chunks = [];
167
+ let status = 0;
168
+ stream.on('response', (headers) => { status = Number(headers[':status'] ?? 0); });
169
+ stream.on('data', (chunk) => chunks.push(chunk));
170
+ stream.on('end', () => resolve({ status, body: Buffer.concat(chunks).toString('utf8') }));
171
+ stream.on('error', (err) => reject(err));
172
+ stream.end(req.body);
173
+ });
174
+ }
175
+ async function close() {
176
+ const s = session;
177
+ session = null;
178
+ if (s)
179
+ await new Promise((resolve) => s.close(() => resolve()));
180
+ }
181
+ return { request, close };
182
+ }
183
+ /** Extract the APNs error `reason` field from a response body, if present. */
184
+ function apnsReason(body) {
185
+ if (!body)
186
+ return null;
187
+ try {
188
+ const parsed = JSON.parse(body);
189
+ return typeof parsed.reason === 'string' ? parsed.reason : null;
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ }
195
+ //# sourceMappingURL=push-notifier-apns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-notifier-apns.js","sourceRoot":"","sources":["../src/push-notifier-apns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * push-notifier-fcm.ts — FCM HTTP v1 strand-wake delivery.
3
+ *
4
+ * Server-only: mints a Google OAuth2 access token (RS256 JWT bearer grant signed
5
+ * with `node:crypto`) and POSTs a data message to the FCM v1 endpoint. The legacy
6
+ * server-key `fcm.googleapis.com/fcm/send` API is deprecated, so we use HTTP v1.
7
+ *
8
+ * The network call is behind an injected `fetch`-like seam so unit tests assert
9
+ * the request shape and map every documented response code with no real network
10
+ * and no credentials. Failures are returned as {@link PushSendResult} values.
11
+ *
12
+ * Secret hygiene: the service-account `privateKey`, the minted JWT, and full
13
+ * device tokens are never logged — failure log lines carry only a status/error
14
+ * code and a redacted token prefix.
15
+ */
16
+ import type { FcmCredentials } from './types.js';
17
+ import type { PushNotifier } from './push-notifier.js';
18
+ /** Minimal `fetch` Response shape the FCM sender consumes. */
19
+ export interface FcmResponseLike {
20
+ status: number;
21
+ json(): Promise<unknown>;
22
+ text(): Promise<string>;
23
+ }
24
+ /** Injected `fetch`-like transport (default: global `fetch`). */
25
+ export type FcmFetch = (url: string, init: {
26
+ method: string;
27
+ headers: Record<string, string>;
28
+ body: string;
29
+ }) => Promise<FcmResponseLike>;
30
+ export interface FcmPushDeps {
31
+ /** Network transport. Defaults to global `fetch`. */
32
+ fetch?: FcmFetch;
33
+ /** Monotonic clock (ms). Defaults to `Date.now`. Injected for cache tests. */
34
+ now?: () => number;
35
+ /** Failure log seam (default: `debug('sereus:cadre:push:fcm')`). */
36
+ log?: (line: string) => void;
37
+ }
38
+ export declare function createFcmPushNotifier(creds: FcmCredentials, deps?: FcmPushDeps): PushNotifier;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * push-notifier-fcm.ts — FCM HTTP v1 strand-wake delivery.
3
+ *
4
+ * Server-only: mints a Google OAuth2 access token (RS256 JWT bearer grant signed
5
+ * with `node:crypto`) and POSTs a data message to the FCM v1 endpoint. The legacy
6
+ * server-key `fcm.googleapis.com/fcm/send` API is deprecated, so we use HTTP v1.
7
+ *
8
+ * The network call is behind an injected `fetch`-like seam so unit tests assert
9
+ * the request shape and map every documented response code with no real network
10
+ * and no credentials. Failures are returned as {@link PushSendResult} values.
11
+ *
12
+ * Secret hygiene: the service-account `privateKey`, the minted JWT, and full
13
+ * device tokens are never logged — failure log lines carry only a status/error
14
+ * code and a redacted token prefix.
15
+ */
16
+ // Namespace import (not `import { sign }`): this module is server-only and is
17
+ // reached only via the guarded dynamic import in CadreNode, but bundlers still
18
+ // analyse that lazy chunk. A *named* import of `node:crypto` hard-fails the
19
+ // browser build ("sign is not exported by __vite-browser-external"), whereas a
20
+ // namespace import of the deliberately-unaliased external only warns (matching
21
+ // the `node:http2` namespace import in push-notifier-apns.ts). The browser
22
+ // never executes this code (config.push is node-only), so the warning is benign.
23
+ import * as nodeCrypto from 'node:crypto';
24
+ import debug from 'debug';
25
+ import { MAX_REASON_LEN, b64urlJson, errText, redact } from './push-notifier-shared.js';
26
+ const TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';
27
+ const FCM_SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';
28
+ const JWT_BEARER_GRANT = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
29
+ /** Re-mint the access token this many ms before its stated expiry. */
30
+ const TOKEN_REFRESH_SKEW_MS = 60000;
31
+ /** OAuth2 access tokens are short-lived (≤1h); assume 1h when unstated. */
32
+ const DEFAULT_TOKEN_TTL_MS = 3600000;
33
+ const debugFcm = debug('sereus:cadre:push:fcm');
34
+ export function createFcmPushNotifier(creds, deps = {}) {
35
+ const doFetch = deps.fetch ?? ((url, init) => fetch(url, init));
36
+ const now = deps.now ?? (() => Date.now());
37
+ const log = deps.log ?? ((line) => debugFcm(line));
38
+ const sendUrl = `https://fcm.googleapis.com/v1/projects/${creds.projectId}/messages:send`;
39
+ let cached = null;
40
+ async function send(msg) {
41
+ let token;
42
+ try {
43
+ token = await accessToken(false);
44
+ }
45
+ catch (err) {
46
+ log(`oauth mint failed: ${errText(err)}`);
47
+ return { ok: false, unregistered: false, error: `fcm oauth mint failed: ${errText(err)}` };
48
+ }
49
+ const first = await postSend(msg, token);
50
+ if (first.status !== 401)
51
+ return mapResult(first, msg.token);
52
+ // The cached access token was rejected (expired/revoked mid-flight). Re-mint
53
+ // once and retry the single send; a second failure is returned as-is.
54
+ let retryToken;
55
+ try {
56
+ retryToken = await accessToken(true);
57
+ }
58
+ catch (err) {
59
+ log(`oauth re-mint failed: ${errText(err)}`);
60
+ return { ok: false, unregistered: false, error: `fcm oauth re-mint failed: ${errText(err)}` };
61
+ }
62
+ return mapResult(await postSend(msg, retryToken), msg.token);
63
+ }
64
+ async function postSend(msg, token) {
65
+ const body = JSON.stringify({
66
+ message: {
67
+ token: msg.token,
68
+ data: {
69
+ type: msg.payload.type,
70
+ strandId: msg.payload.strandId,
71
+ reason: msg.payload.reason.slice(0, MAX_REASON_LEN),
72
+ },
73
+ android: { priority: 'high' },
74
+ },
75
+ });
76
+ const res = await doFetch(sendUrl, {
77
+ method: 'POST',
78
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
79
+ body,
80
+ });
81
+ return { status: res.status, body: await safeText(res) };
82
+ }
83
+ function mapResult(res, deviceToken) {
84
+ if (res.status === 200)
85
+ return { ok: true };
86
+ const summary = errorSummary(res.status, res.body);
87
+ if (isUnregistered(res.status, res.body)) {
88
+ log(`unregistered token=${redact(deviceToken)} (${summary})`);
89
+ return { ok: false, unregistered: true, error: summary };
90
+ }
91
+ log(`send failed token=${redact(deviceToken)} (${summary})`);
92
+ return { ok: false, unregistered: false, error: summary };
93
+ }
94
+ async function accessToken(forceRefresh) {
95
+ if (!forceRefresh && cached && cached.expiresAt > now())
96
+ return cached.value;
97
+ cached = null;
98
+ const minted = await mintAccessToken();
99
+ cached = { value: minted.value, expiresAt: now() + minted.ttlMs - TOKEN_REFRESH_SKEW_MS };
100
+ return minted.value;
101
+ }
102
+ async function mintAccessToken() {
103
+ const jwt = signOauthJwt();
104
+ const res = await doFetch(TOKEN_ENDPOINT, {
105
+ method: 'POST',
106
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
107
+ body: `grant_type=${encodeURIComponent(JWT_BEARER_GRANT)}&assertion=${encodeURIComponent(jwt)}`,
108
+ });
109
+ if (res.status !== 200) {
110
+ throw new Error(`token endpoint ${res.status}: ${await safeText(res)}`);
111
+ }
112
+ const parsed = (await res.json());
113
+ if (typeof parsed.access_token !== 'string') {
114
+ throw new Error('token endpoint returned no access_token');
115
+ }
116
+ const ttlMs = typeof parsed.expires_in === 'number' ? parsed.expires_in * 1000 : DEFAULT_TOKEN_TTL_MS;
117
+ return { value: parsed.access_token, ttlMs };
118
+ }
119
+ function signOauthJwt() {
120
+ const iat = Math.floor(now() / 1000);
121
+ const header = { alg: 'RS256', typ: 'JWT' };
122
+ const claims = {
123
+ iss: creds.clientEmail,
124
+ sub: creds.clientEmail,
125
+ scope: FCM_SCOPE,
126
+ aud: TOKEN_ENDPOINT,
127
+ iat,
128
+ exp: iat + 3600,
129
+ };
130
+ const signingInput = `${b64urlJson(header)}.${b64urlJson(claims)}`;
131
+ const sig = nodeCrypto.sign('RSA-SHA256', Buffer.from(signingInput), creds.privateKey).toString('base64url');
132
+ return `${signingInput}.${sig}`;
133
+ }
134
+ async function close() {
135
+ cached = null;
136
+ }
137
+ return { send, close };
138
+ }
139
+ /**
140
+ * Whether an FCM v1 error response names a permanently-invalid registration
141
+ * token: a 404 (`UNREGISTERED`), or a 400 `INVALID_ARGUMENT` whose message names
142
+ * the registration token. A generic 400 (malformed message) is NOT unregistered.
143
+ */
144
+ function isUnregistered(status, body) {
145
+ if (status === 404)
146
+ return true;
147
+ if (status !== 400)
148
+ return false;
149
+ const code = fcmErrorCode(body);
150
+ if (code === 'UNREGISTERED')
151
+ return true;
152
+ if (code === 'INVALID_ARGUMENT' && /registration[ -]token|not a valid fcm registration/i.test(body))
153
+ return true;
154
+ return false;
155
+ }
156
+ /** Pull the FCM `errorCode` (FcmError detail) or RPC `status` out of the body. */
157
+ function fcmErrorCode(body) {
158
+ try {
159
+ const parsed = JSON.parse(body);
160
+ const err = parsed.error;
161
+ if (!err)
162
+ return null;
163
+ const detailCode = err.details?.find((d) => typeof d.errorCode === 'string')?.errorCode;
164
+ return detailCode ?? err.status ?? null;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ /** A secret-free one-line error summary (status + error code, never the token). */
171
+ function errorSummary(status, body) {
172
+ const code = fcmErrorCode(body);
173
+ return code ? `fcm ${status} ${code}` : `fcm ${status}`;
174
+ }
175
+ async function safeText(res) {
176
+ try {
177
+ return await res.text();
178
+ }
179
+ catch {
180
+ return '';
181
+ }
182
+ }
183
+ //# sourceMappingURL=push-notifier-fcm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-notifier-fcm.js","sourceRoot":"","sources":["../src/push-notifier-fcm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * push-notifier-shared.ts — internal helpers shared by the FCM and APNs
3
+ * push-notifier implementations. Server-only (used only by modules that already
4
+ * reach for `node:crypto` / `node:http2`); never re-exported from the package
5
+ * entry. Centralized so the secret-redaction and payload-bounding rules have a
6
+ * single, hardenable source rather than drifting between the two transports.
7
+ */
8
+ /** Defensive cap on the free-form `reason` carried in a strand-wake payload. */
9
+ export declare const MAX_REASON_LEN = 256;
10
+ /** Base64url-encode the JSON form of an object (JWT header/claims segments). */
11
+ export declare function b64urlJson(obj: unknown): string;
12
+ /** A short, secret-free string for an unknown thrown value. */
13
+ export declare function errText(err: unknown): string;
14
+ /**
15
+ * Redact a device token to a short, non-reversible prefix for debug lines — the
16
+ * full FCM/APNs token is a secret and must never reach a log.
17
+ */
18
+ export declare function redact(token: string): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * push-notifier-shared.ts — internal helpers shared by the FCM and APNs
3
+ * push-notifier implementations. Server-only (used only by modules that already
4
+ * reach for `node:crypto` / `node:http2`); never re-exported from the package
5
+ * entry. Centralized so the secret-redaction and payload-bounding rules have a
6
+ * single, hardenable source rather than drifting between the two transports.
7
+ */
8
+ /** Defensive cap on the free-form `reason` carried in a strand-wake payload. */
9
+ export const MAX_REASON_LEN = 256;
10
+ /** Base64url-encode the JSON form of an object (JWT header/claims segments). */
11
+ export function b64urlJson(obj) {
12
+ return Buffer.from(JSON.stringify(obj)).toString('base64url');
13
+ }
14
+ /** A short, secret-free string for an unknown thrown value. */
15
+ export function errText(err) {
16
+ return err instanceof Error ? err.message : String(err);
17
+ }
18
+ /**
19
+ * Redact a device token to a short, non-reversible prefix for debug lines — the
20
+ * full FCM/APNs token is a secret and must never reach a log.
21
+ */
22
+ export function redact(token) {
23
+ return token.length <= 8 ? '…' : `${token.slice(0, 6)}…`;
24
+ }
25
+ //# sourceMappingURL=push-notifier-shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-notifier-shared.js","sourceRoot":"","sources":["../src/push-notifier-shared.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAElC,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,GAAY;IACrC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAChE,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,OAAO,CAAC,GAAY;IAClC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * push-notifier.ts — the platform-push **delivery** seam for strand-wake.
3
+ *
4
+ * Given a resolved mobile peer token, send one `strand-wake` data message over
5
+ * the right platform channel (FCM for Android, APNs for iOS). This module owns no
6
+ * policy — *who* to wake and *when* is the fan-out (`push-fanout.ts`). It is the
7
+ * reusable, credential-injected, transport-injected unit the fan-out constructs
8
+ * inside a participating `CadreNode` when push credentials are configured.
9
+ *
10
+ * `createPushNotifier` returns a router that dispatches by `msg.platform` to an
11
+ * `FcmPushNotifier` and/or `ApnsPushNotifier`, constructing only the
12
+ * implementations whose credentials are present. Failures are values, never
13
+ * thrown — mirroring `ServiceWakeResult` / `resolveDeviceToken`'s no-throw
14
+ * conventions so a best-effort fan-out can branch on the outcome.
15
+ *
16
+ * Server-only by construction: the FCM/APNs implementations reach for
17
+ * `node:crypto` / `node:http2`. They are never pulled into the RN/browser bundle
18
+ * because the cross-platform `index.ts` re-exports only the *types* here (a
19
+ * type-only re-export is erased at emit), and the only runtime constructor of a
20
+ * notifier is the in-package fan-out that runs inside a Node `CadreNode`. A
21
+ * cross-platform consumer imports the payload contract + types, never this value.
22
+ */
23
+ import type { PushPlatform, PushCredentials } from './types.js';
24
+ import type { StrandWakePayload } from './strand-wake-payload.js';
25
+ import { type FcmPushDeps } from './push-notifier-fcm.js';
26
+ import { type ApnsPushDeps } from './push-notifier-apns.js';
27
+ /** One strand-wake data message addressed to one device. */
28
+ export interface PushMessage {
29
+ /** The resolved `DeviceToken.token` for the target peer. */
30
+ token: string;
31
+ /** Which channel — selects FCM vs APNs. */
32
+ platform: PushPlatform;
33
+ /** The strand-wake payload `{ type:'strand-wake', strandId, reason }`. */
34
+ payload: StrandWakePayload;
35
+ }
36
+ /**
37
+ * Outcome of a single {@link PushNotifier.send}. A delivery failure is a value,
38
+ * never a thrown error. `unregistered: true` means the platform reported the
39
+ * token is permanently invalid (FCM 404 `UNREGISTERED` / 400 naming the token;
40
+ * APNs 410 `Unregistered` / 400 `BadDeviceToken`) — the caller should expire the
41
+ * stale `DeviceToken`. Any other failure is `unregistered: false` and is treated
42
+ * as best-effort/transient (no retry storm).
43
+ */
44
+ export type PushSendResult = {
45
+ ok: true;
46
+ } | {
47
+ ok: false;
48
+ unregistered: boolean;
49
+ error: string;
50
+ };
51
+ export interface PushNotifier {
52
+ /** Send one strand-wake data message to one device. Never throws. */
53
+ send(msg: PushMessage): Promise<PushSendResult>;
54
+ /** Release transport resources (e.g. the APNs HTTP/2 session). */
55
+ close(): Promise<void>;
56
+ }
57
+ /**
58
+ * Per-platform transport/clock/log seams, injected by tests. Defaults are the
59
+ * real implementations (global `fetch` for FCM, a `node:http2` session for APNs).
60
+ */
61
+ export interface PushNotifierDeps {
62
+ fcm?: FcmPushDeps;
63
+ apns?: ApnsPushDeps;
64
+ }
65
+ /**
66
+ * Build a {@link PushNotifier} router over the configured credentials. Only the
67
+ * platforms whose credentials are present get a backing implementation; a `send`
68
+ * for an unconfigured platform returns a best-effort `no <platform> credentials`
69
+ * failure rather than throwing.
70
+ */
71
+ export declare function createPushNotifier(creds: PushCredentials, deps?: PushNotifierDeps): PushNotifier;