@threadbase-sh/streamer 1.48.0 → 1.49.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/cli.cjs +195 -175
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +189 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +186 -168
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1706,6 +1706,13 @@ type ApiDeps = {
|
|
|
1706
1706
|
cacheMonitor: () => CacheIntegrityMonitor | null;
|
|
1707
1707
|
/** Push registration + delivery state (C7). Null when the cache DB is unavailable. */
|
|
1708
1708
|
pushRepo: () => PushRepository | null;
|
|
1709
|
+
/**
|
|
1710
|
+
* Whether Live Activity push is actually running on this server — the same
|
|
1711
|
+
* fact the boot log reports as `live_activity.enabled`. Asked of the server
|
|
1712
|
+
* rather than re-derived from the environment, because APNs credentials alone
|
|
1713
|
+
* do not enable it: the sender is only wired when the token store opened too.
|
|
1714
|
+
*/
|
|
1715
|
+
liveActivityPushEnabled: () => boolean;
|
|
1709
1716
|
/** Paired-device registry (C5). Null when the cache DB is unavailable. */
|
|
1710
1717
|
devicesRepo: () => DevicesRepository | null;
|
|
1711
1718
|
projectsRepo: () => ProjectsRepository | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1706,6 +1706,13 @@ type ApiDeps = {
|
|
|
1706
1706
|
cacheMonitor: () => CacheIntegrityMonitor | null;
|
|
1707
1707
|
/** Push registration + delivery state (C7). Null when the cache DB is unavailable. */
|
|
1708
1708
|
pushRepo: () => PushRepository | null;
|
|
1709
|
+
/**
|
|
1710
|
+
* Whether Live Activity push is actually running on this server — the same
|
|
1711
|
+
* fact the boot log reports as `live_activity.enabled`. Asked of the server
|
|
1712
|
+
* rather than re-derived from the environment, because APNs credentials alone
|
|
1713
|
+
* do not enable it: the sender is only wired when the token store opened too.
|
|
1714
|
+
*/
|
|
1715
|
+
liveActivityPushEnabled: () => boolean;
|
|
1709
1716
|
/** Paired-device registry (C5). Null when the cache DB is unavailable. */
|
|
1710
1717
|
devicesRepo: () => DevicesRepository | null;
|
|
1711
1718
|
projectsRepo: () => ProjectsRepository | null;
|
package/dist/index.js
CHANGED
|
@@ -5550,6 +5550,167 @@ var PushRepository = class {
|
|
|
5550
5550
|
}
|
|
5551
5551
|
};
|
|
5552
5552
|
|
|
5553
|
+
// src/services/push/apnsClient.ts
|
|
5554
|
+
import { createSign } from "crypto";
|
|
5555
|
+
import { connect, constants } from "http2";
|
|
5556
|
+
var log3 = getLogger("apns");
|
|
5557
|
+
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
5558
|
+
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
5559
|
+
var JWT_TTL_SECONDS = 3e3;
|
|
5560
|
+
var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
|
|
5561
|
+
"BadDeviceToken",
|
|
5562
|
+
"DeviceTokenNotForTopic",
|
|
5563
|
+
"Unregistered",
|
|
5564
|
+
"ExpiredToken"
|
|
5565
|
+
]);
|
|
5566
|
+
function base64url(input) {
|
|
5567
|
+
return Buffer.from(input).toString("base64url");
|
|
5568
|
+
}
|
|
5569
|
+
function readApnsCredentialsFromEnv(env = process.env) {
|
|
5570
|
+
const key = env.APNS_KEY;
|
|
5571
|
+
if (!key || key.trim().length === 0) return null;
|
|
5572
|
+
const keyId = env.APNS_KEY_ID?.trim();
|
|
5573
|
+
const teamId = env.APNS_TEAM_ID?.trim();
|
|
5574
|
+
const bundleId = env.APNS_BUNDLE_ID?.trim();
|
|
5575
|
+
if (!keyId || !teamId || !bundleId) return null;
|
|
5576
|
+
const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
|
|
5577
|
+
return { key, keyId, teamId, bundleId, host };
|
|
5578
|
+
}
|
|
5579
|
+
function describeMissingApnsCredentials(env = process.env) {
|
|
5580
|
+
if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
|
|
5581
|
+
return "APNS_KEY is not set, so Live Activity push is disabled. Set it to the p8 key contents (not a path) to enable it.";
|
|
5582
|
+
}
|
|
5583
|
+
const missing = [
|
|
5584
|
+
["APNS_KEY_ID", env.APNS_KEY_ID],
|
|
5585
|
+
["APNS_TEAM_ID", env.APNS_TEAM_ID],
|
|
5586
|
+
["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
|
|
5587
|
+
].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
|
|
5588
|
+
if (missing.length === 0) return null;
|
|
5589
|
+
return `APNS_KEY is set but ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not, so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.`;
|
|
5590
|
+
}
|
|
5591
|
+
var ApnsClient = class {
|
|
5592
|
+
constructor(creds) {
|
|
5593
|
+
this.creds = creds;
|
|
5594
|
+
}
|
|
5595
|
+
creds;
|
|
5596
|
+
session = null;
|
|
5597
|
+
cachedJwt = null;
|
|
5598
|
+
/**
|
|
5599
|
+
* The `apns-topic` for Live Activity pushes.
|
|
5600
|
+
*
|
|
5601
|
+
* The `.push-type.liveactivity` suffix is mandatory and is why the signing key
|
|
5602
|
+
* must be Team Scoped (All Topics) — a key scoped to the bundle id alone
|
|
5603
|
+
* cannot sign this topic.
|
|
5604
|
+
*/
|
|
5605
|
+
get topic() {
|
|
5606
|
+
return `${this.creds.bundleId}.push-type.liveactivity`;
|
|
5607
|
+
}
|
|
5608
|
+
/**
|
|
5609
|
+
* Mint or reuse the provider JWT.
|
|
5610
|
+
*
|
|
5611
|
+
* ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
|
|
5612
|
+
* token older than an hour, but minting one per request is wasteful and can
|
|
5613
|
+
* trip APNs' provider-token-update throttle.
|
|
5614
|
+
*/
|
|
5615
|
+
getJwt(now = Date.now()) {
|
|
5616
|
+
const nowSeconds = Math.floor(now / 1e3);
|
|
5617
|
+
if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
|
|
5618
|
+
return this.cachedJwt.token;
|
|
5619
|
+
}
|
|
5620
|
+
const header = base64url(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
|
|
5621
|
+
const payload = base64url(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
|
|
5622
|
+
const signingInput = `${header}.${payload}`;
|
|
5623
|
+
const signature = createSign("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
|
|
5624
|
+
const token = `${signingInput}.${base64url(signature)}`;
|
|
5625
|
+
this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
|
|
5626
|
+
return token;
|
|
5627
|
+
}
|
|
5628
|
+
/**
|
|
5629
|
+
* Reuse one HTTP/2 session across sends.
|
|
5630
|
+
*
|
|
5631
|
+
* APNs expects a long-lived connection; a fresh TLS handshake per push is slow
|
|
5632
|
+
* and Apple treats connection churn as abuse.
|
|
5633
|
+
*/
|
|
5634
|
+
getSession() {
|
|
5635
|
+
if (this.session && !this.session.closed && !this.session.destroyed) {
|
|
5636
|
+
return this.session;
|
|
5637
|
+
}
|
|
5638
|
+
const session = connect(`https://${this.creds.host}`);
|
|
5639
|
+
session.on("error", (err) => {
|
|
5640
|
+
log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
5641
|
+
});
|
|
5642
|
+
this.session = session;
|
|
5643
|
+
return session;
|
|
5644
|
+
}
|
|
5645
|
+
/**
|
|
5646
|
+
* Send one push.
|
|
5647
|
+
*
|
|
5648
|
+
* Resolves with a result rather than rejecting on an APNs rejection: a
|
|
5649
|
+
* rejected push is an expected outcome the caller must act on (retire the
|
|
5650
|
+
* token), not an exception. Only a genuinely unexpected local failure throws,
|
|
5651
|
+
* and the caller logs it.
|
|
5652
|
+
*/
|
|
5653
|
+
async send(args) {
|
|
5654
|
+
const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
|
|
5655
|
+
if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
|
|
5656
|
+
throw new Error(
|
|
5657
|
+
`APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
|
|
5658
|
+
);
|
|
5659
|
+
}
|
|
5660
|
+
const session = this.getSession();
|
|
5661
|
+
const headers = {
|
|
5662
|
+
[constants.HTTP2_HEADER_METHOD]: "POST",
|
|
5663
|
+
[constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
5664
|
+
[constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
5665
|
+
"apns-push-type": "liveactivity",
|
|
5666
|
+
"apns-topic": this.topic,
|
|
5667
|
+
"apns-priority": String(args.priority ?? 10),
|
|
5668
|
+
...args.expirationSeconds != null && {
|
|
5669
|
+
"apns-expiration": String(args.expirationSeconds)
|
|
5670
|
+
},
|
|
5671
|
+
[constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
5672
|
+
[constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
5673
|
+
};
|
|
5674
|
+
return new Promise((resolve2, reject) => {
|
|
5675
|
+
const req = session.request(headers);
|
|
5676
|
+
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
5677
|
+
req.close(constants.NGHTTP2_CANCEL);
|
|
5678
|
+
resolve2({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
5679
|
+
});
|
|
5680
|
+
let status = 0;
|
|
5681
|
+
req.on("response", (resHeaders) => {
|
|
5682
|
+
status = Number(resHeaders[constants.HTTP2_HEADER_STATUS] ?? 0);
|
|
5683
|
+
});
|
|
5684
|
+
const chunks = [];
|
|
5685
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
5686
|
+
req.on("error", reject);
|
|
5687
|
+
req.on("end", () => {
|
|
5688
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
5689
|
+
let reason;
|
|
5690
|
+
if (raw.length > 0) {
|
|
5691
|
+
try {
|
|
5692
|
+
reason = JSON.parse(raw).reason;
|
|
5693
|
+
} catch {
|
|
5694
|
+
reason = raw.slice(0, 200);
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
resolve2({
|
|
5698
|
+
ok: status === 200,
|
|
5699
|
+
status,
|
|
5700
|
+
reason,
|
|
5701
|
+
tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
|
|
5702
|
+
});
|
|
5703
|
+
});
|
|
5704
|
+
req.end(body);
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
/** Close the shared connection. Called on server shutdown. */
|
|
5708
|
+
close() {
|
|
5709
|
+
this.session?.close();
|
|
5710
|
+
this.session = null;
|
|
5711
|
+
}
|
|
5712
|
+
};
|
|
5713
|
+
|
|
5553
5714
|
// src/api/routes/misc.routes.ts
|
|
5554
5715
|
function numberOrNull(value) {
|
|
5555
5716
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
@@ -5586,6 +5747,17 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
5586
5747
|
if (a.length !== b.length) return false;
|
|
5587
5748
|
return timingSafeEqual3(a, b);
|
|
5588
5749
|
}
|
|
5750
|
+
function describePushCapability(liveActivityEnabled, env = process.env) {
|
|
5751
|
+
if (liveActivityEnabled) return { liveActivity: true, notifications: false };
|
|
5752
|
+
return {
|
|
5753
|
+
liveActivity: false,
|
|
5754
|
+
notifications: false,
|
|
5755
|
+
// describeMissingApnsCredentials only explains a *credential* gap and
|
|
5756
|
+
// returns null once the credentials are complete — reachable here, because
|
|
5757
|
+
// an unavailable token store disables the feature with the key still set.
|
|
5758
|
+
liveActivityReason: describeMissingApnsCredentials(env) ?? "APNs credentials are set but the push token store is unavailable, so Live Activity push is disabled."
|
|
5759
|
+
};
|
|
5760
|
+
}
|
|
5589
5761
|
var clientLog = getLogger("client");
|
|
5590
5762
|
var createMiscRoutes = (deps) => {
|
|
5591
5763
|
const app = new Hono11();
|
|
@@ -5607,7 +5779,12 @@ var createMiscRoutes = (deps) => {
|
|
|
5607
5779
|
featureFlags: true,
|
|
5608
5780
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
5609
5781
|
// Hub's grouped views need before they can draw a tree.
|
|
5610
|
-
projectSummary: true
|
|
5782
|
+
projectSummary: true,
|
|
5783
|
+
// Delivery capability, not endpoint support: whether this server can
|
|
5784
|
+
// actually send a push, so mobile can hide an affordance instead of
|
|
5785
|
+
// registering tokens nothing will ever send to. Absent on older servers,
|
|
5786
|
+
// which a client should read as "unknown", not "unavailable".
|
|
5787
|
+
push: describePushCapability(deps.liveActivityPushEnabled())
|
|
5611
5788
|
});
|
|
5612
5789
|
});
|
|
5613
5790
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -5668,9 +5845,10 @@ var createMiscRoutes = (deps) => {
|
|
|
5668
5845
|
return c.json({ ok: true });
|
|
5669
5846
|
});
|
|
5670
5847
|
app.get("/api/push/health", (c) => {
|
|
5848
|
+
const push = describePushCapability(deps.liveActivityPushEnabled());
|
|
5671
5849
|
const repo = deps.pushRepo();
|
|
5672
|
-
if (!repo) return c.json({ tokens: [], available: false });
|
|
5673
|
-
return c.json({ tokens: repo.listHealth(), available: true });
|
|
5850
|
+
if (!repo) return c.json({ tokens: [], available: false, push });
|
|
5851
|
+
return c.json({ tokens: repo.listHealth(), available: true, push });
|
|
5674
5852
|
});
|
|
5675
5853
|
app.post("/api/__update", async (c) => {
|
|
5676
5854
|
const cfg = loadUpdateConfig();
|
|
@@ -6172,7 +6350,7 @@ import { dirname as dirname7 } from "path";
|
|
|
6172
6350
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
6173
6351
|
|
|
6174
6352
|
// src/db/query-timing.ts
|
|
6175
|
-
var
|
|
6353
|
+
var log4 = getLogger("db");
|
|
6176
6354
|
var DEFAULT_SLOW_QUERY_MS = 35;
|
|
6177
6355
|
var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
|
|
6178
6356
|
function deriveLabel(sql) {
|
|
@@ -6188,15 +6366,15 @@ function resolveSlowMs() {
|
|
|
6188
6366
|
}
|
|
6189
6367
|
function record(label, ms, rows, slowMs) {
|
|
6190
6368
|
if (slowMs > 0 && ms >= slowMs) {
|
|
6191
|
-
|
|
6369
|
+
log4.warn(
|
|
6192
6370
|
`[db] slow query ${label} ${ms.toFixed(1)}ms rows=${rows}`,
|
|
6193
6371
|
{ event: "db.slow_query", stmt: label, ms: Math.round(ms * 100) / 100, rows },
|
|
6194
6372
|
"pino"
|
|
6195
6373
|
);
|
|
6196
6374
|
return;
|
|
6197
6375
|
}
|
|
6198
|
-
if (
|
|
6199
|
-
|
|
6376
|
+
if (log4.pino.isLevelEnabled("debug")) {
|
|
6377
|
+
log4.debug(
|
|
6200
6378
|
`[db] ${label} ${ms.toFixed(2)}ms rows=${rows}`,
|
|
6201
6379
|
{ event: "db.query", stmt: label, ms: Math.round(ms * 100) / 100, rows },
|
|
6202
6380
|
"pino"
|
|
@@ -9056,167 +9234,6 @@ function deriveProjectChatTitle(input) {
|
|
|
9056
9234
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
9057
9235
|
}
|
|
9058
9236
|
|
|
9059
|
-
// src/services/push/apnsClient.ts
|
|
9060
|
-
import { createSign } from "crypto";
|
|
9061
|
-
import { connect, constants } from "http2";
|
|
9062
|
-
var log4 = getLogger("apns");
|
|
9063
|
-
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
9064
|
-
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
9065
|
-
var JWT_TTL_SECONDS = 3e3;
|
|
9066
|
-
var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
|
|
9067
|
-
"BadDeviceToken",
|
|
9068
|
-
"DeviceTokenNotForTopic",
|
|
9069
|
-
"Unregistered",
|
|
9070
|
-
"ExpiredToken"
|
|
9071
|
-
]);
|
|
9072
|
-
function base64url(input) {
|
|
9073
|
-
return Buffer.from(input).toString("base64url");
|
|
9074
|
-
}
|
|
9075
|
-
function readApnsCredentialsFromEnv(env = process.env) {
|
|
9076
|
-
const key = env.APNS_KEY;
|
|
9077
|
-
if (!key || key.trim().length === 0) return null;
|
|
9078
|
-
const keyId = env.APNS_KEY_ID?.trim();
|
|
9079
|
-
const teamId = env.APNS_TEAM_ID?.trim();
|
|
9080
|
-
const bundleId = env.APNS_BUNDLE_ID?.trim();
|
|
9081
|
-
if (!keyId || !teamId || !bundleId) return null;
|
|
9082
|
-
const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
|
|
9083
|
-
return { key, keyId, teamId, bundleId, host };
|
|
9084
|
-
}
|
|
9085
|
-
function describeMissingApnsCredentials(env = process.env) {
|
|
9086
|
-
if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
|
|
9087
|
-
return "APNS_KEY is not set, so Live Activity push is disabled. Set it to the p8 key contents (not a path) to enable it.";
|
|
9088
|
-
}
|
|
9089
|
-
const missing = [
|
|
9090
|
-
["APNS_KEY_ID", env.APNS_KEY_ID],
|
|
9091
|
-
["APNS_TEAM_ID", env.APNS_TEAM_ID],
|
|
9092
|
-
["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
|
|
9093
|
-
].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
|
|
9094
|
-
if (missing.length === 0) return null;
|
|
9095
|
-
return `APNS_KEY is set but ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not, so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.`;
|
|
9096
|
-
}
|
|
9097
|
-
var ApnsClient = class {
|
|
9098
|
-
constructor(creds) {
|
|
9099
|
-
this.creds = creds;
|
|
9100
|
-
}
|
|
9101
|
-
creds;
|
|
9102
|
-
session = null;
|
|
9103
|
-
cachedJwt = null;
|
|
9104
|
-
/**
|
|
9105
|
-
* The `apns-topic` for Live Activity pushes.
|
|
9106
|
-
*
|
|
9107
|
-
* The `.push-type.liveactivity` suffix is mandatory and is why the signing key
|
|
9108
|
-
* must be Team Scoped (All Topics) — a key scoped to the bundle id alone
|
|
9109
|
-
* cannot sign this topic.
|
|
9110
|
-
*/
|
|
9111
|
-
get topic() {
|
|
9112
|
-
return `${this.creds.bundleId}.push-type.liveactivity`;
|
|
9113
|
-
}
|
|
9114
|
-
/**
|
|
9115
|
-
* Mint or reuse the provider JWT.
|
|
9116
|
-
*
|
|
9117
|
-
* ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
|
|
9118
|
-
* token older than an hour, but minting one per request is wasteful and can
|
|
9119
|
-
* trip APNs' provider-token-update throttle.
|
|
9120
|
-
*/
|
|
9121
|
-
getJwt(now = Date.now()) {
|
|
9122
|
-
const nowSeconds = Math.floor(now / 1e3);
|
|
9123
|
-
if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
|
|
9124
|
-
return this.cachedJwt.token;
|
|
9125
|
-
}
|
|
9126
|
-
const header = base64url(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
|
|
9127
|
-
const payload = base64url(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
|
|
9128
|
-
const signingInput = `${header}.${payload}`;
|
|
9129
|
-
const signature = createSign("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
|
|
9130
|
-
const token = `${signingInput}.${base64url(signature)}`;
|
|
9131
|
-
this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
|
|
9132
|
-
return token;
|
|
9133
|
-
}
|
|
9134
|
-
/**
|
|
9135
|
-
* Reuse one HTTP/2 session across sends.
|
|
9136
|
-
*
|
|
9137
|
-
* APNs expects a long-lived connection; a fresh TLS handshake per push is slow
|
|
9138
|
-
* and Apple treats connection churn as abuse.
|
|
9139
|
-
*/
|
|
9140
|
-
getSession() {
|
|
9141
|
-
if (this.session && !this.session.closed && !this.session.destroyed) {
|
|
9142
|
-
return this.session;
|
|
9143
|
-
}
|
|
9144
|
-
const session = connect(`https://${this.creds.host}`);
|
|
9145
|
-
session.on("error", (err) => {
|
|
9146
|
-
log4.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
9147
|
-
});
|
|
9148
|
-
this.session = session;
|
|
9149
|
-
return session;
|
|
9150
|
-
}
|
|
9151
|
-
/**
|
|
9152
|
-
* Send one push.
|
|
9153
|
-
*
|
|
9154
|
-
* Resolves with a result rather than rejecting on an APNs rejection: a
|
|
9155
|
-
* rejected push is an expected outcome the caller must act on (retire the
|
|
9156
|
-
* token), not an exception. Only a genuinely unexpected local failure throws,
|
|
9157
|
-
* and the caller logs it.
|
|
9158
|
-
*/
|
|
9159
|
-
async send(args) {
|
|
9160
|
-
const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
|
|
9161
|
-
if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
|
|
9162
|
-
throw new Error(
|
|
9163
|
-
`APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
|
|
9164
|
-
);
|
|
9165
|
-
}
|
|
9166
|
-
const session = this.getSession();
|
|
9167
|
-
const headers = {
|
|
9168
|
-
[constants.HTTP2_HEADER_METHOD]: "POST",
|
|
9169
|
-
[constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
9170
|
-
[constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
9171
|
-
"apns-push-type": "liveactivity",
|
|
9172
|
-
"apns-topic": this.topic,
|
|
9173
|
-
"apns-priority": String(args.priority ?? 10),
|
|
9174
|
-
...args.expirationSeconds != null && {
|
|
9175
|
-
"apns-expiration": String(args.expirationSeconds)
|
|
9176
|
-
},
|
|
9177
|
-
[constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
9178
|
-
[constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
9179
|
-
};
|
|
9180
|
-
return new Promise((resolve2, reject) => {
|
|
9181
|
-
const req = session.request(headers);
|
|
9182
|
-
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
9183
|
-
req.close(constants.NGHTTP2_CANCEL);
|
|
9184
|
-
resolve2({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
9185
|
-
});
|
|
9186
|
-
let status = 0;
|
|
9187
|
-
req.on("response", (resHeaders) => {
|
|
9188
|
-
status = Number(resHeaders[constants.HTTP2_HEADER_STATUS] ?? 0);
|
|
9189
|
-
});
|
|
9190
|
-
const chunks = [];
|
|
9191
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
9192
|
-
req.on("error", reject);
|
|
9193
|
-
req.on("end", () => {
|
|
9194
|
-
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
9195
|
-
let reason;
|
|
9196
|
-
if (raw.length > 0) {
|
|
9197
|
-
try {
|
|
9198
|
-
reason = JSON.parse(raw).reason;
|
|
9199
|
-
} catch {
|
|
9200
|
-
reason = raw.slice(0, 200);
|
|
9201
|
-
}
|
|
9202
|
-
}
|
|
9203
|
-
resolve2({
|
|
9204
|
-
ok: status === 200,
|
|
9205
|
-
status,
|
|
9206
|
-
reason,
|
|
9207
|
-
tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
|
|
9208
|
-
});
|
|
9209
|
-
});
|
|
9210
|
-
req.end(body);
|
|
9211
|
-
});
|
|
9212
|
-
}
|
|
9213
|
-
/** Close the shared connection. Called on server shutdown. */
|
|
9214
|
-
close() {
|
|
9215
|
-
this.session?.close();
|
|
9216
|
-
this.session = null;
|
|
9217
|
-
}
|
|
9218
|
-
};
|
|
9219
|
-
|
|
9220
9237
|
// src/services/push/expoPushSender.ts
|
|
9221
9238
|
var log5 = getLogger("expo-push");
|
|
9222
9239
|
var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
|
|
@@ -11489,6 +11506,7 @@ var StreamerServer = class {
|
|
|
11489
11506
|
cache: () => this.cache,
|
|
11490
11507
|
cacheMonitor: () => this.cacheMonitor,
|
|
11491
11508
|
pushRepo: () => this.pushRepo,
|
|
11509
|
+
liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
|
|
11492
11510
|
devicesRepo: () => this.devicesRepo,
|
|
11493
11511
|
projectsRepo: () => this.projectsRepo,
|
|
11494
11512
|
conversationsRepo: () => this.conversationsRepo,
|