@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/cli.cjs
CHANGED
|
@@ -142369,7 +142369,7 @@ function createLogsRoutes() {
|
|
|
142369
142369
|
|
|
142370
142370
|
// src/api/routes/misc.routes.ts
|
|
142371
142371
|
var import_node_child_process2 = require("child_process");
|
|
142372
|
-
var
|
|
142372
|
+
var import_node_crypto2 = require("crypto");
|
|
142373
142373
|
var import_os7 = require("os");
|
|
142374
142374
|
|
|
142375
142375
|
// src/db/repositories/push.repository.ts
|
|
@@ -142628,6 +142628,170 @@ var PushRepository = class {
|
|
|
142628
142628
|
|
|
142629
142629
|
// src/api/routes/misc.routes.ts
|
|
142630
142630
|
init_logger();
|
|
142631
|
+
|
|
142632
|
+
// src/services/push/apnsClient.ts
|
|
142633
|
+
var import_node_crypto = require("crypto");
|
|
142634
|
+
var import_node_http2 = require("http2");
|
|
142635
|
+
init_logger();
|
|
142636
|
+
var log3 = getLogger("apns");
|
|
142637
|
+
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
142638
|
+
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
142639
|
+
var JWT_TTL_SECONDS = 3e3;
|
|
142640
|
+
var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
|
|
142641
|
+
"BadDeviceToken",
|
|
142642
|
+
"DeviceTokenNotForTopic",
|
|
142643
|
+
"Unregistered",
|
|
142644
|
+
"ExpiredToken"
|
|
142645
|
+
]);
|
|
142646
|
+
function base64url3(input) {
|
|
142647
|
+
return Buffer.from(input).toString("base64url");
|
|
142648
|
+
}
|
|
142649
|
+
function readApnsCredentialsFromEnv(env = process.env) {
|
|
142650
|
+
const key = env.APNS_KEY;
|
|
142651
|
+
if (!key || key.trim().length === 0) return null;
|
|
142652
|
+
const keyId = env.APNS_KEY_ID?.trim();
|
|
142653
|
+
const teamId = env.APNS_TEAM_ID?.trim();
|
|
142654
|
+
const bundleId = env.APNS_BUNDLE_ID?.trim();
|
|
142655
|
+
if (!keyId || !teamId || !bundleId) return null;
|
|
142656
|
+
const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
|
|
142657
|
+
return { key, keyId, teamId, bundleId, host };
|
|
142658
|
+
}
|
|
142659
|
+
function describeMissingApnsCredentials(env = process.env) {
|
|
142660
|
+
if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
|
|
142661
|
+
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.";
|
|
142662
|
+
}
|
|
142663
|
+
const missing = [
|
|
142664
|
+
["APNS_KEY_ID", env.APNS_KEY_ID],
|
|
142665
|
+
["APNS_TEAM_ID", env.APNS_TEAM_ID],
|
|
142666
|
+
["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
|
|
142667
|
+
].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
|
|
142668
|
+
if (missing.length === 0) return null;
|
|
142669
|
+
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.`;
|
|
142670
|
+
}
|
|
142671
|
+
var ApnsClient = class {
|
|
142672
|
+
constructor(creds) {
|
|
142673
|
+
this.creds = creds;
|
|
142674
|
+
}
|
|
142675
|
+
creds;
|
|
142676
|
+
session = null;
|
|
142677
|
+
cachedJwt = null;
|
|
142678
|
+
/**
|
|
142679
|
+
* The `apns-topic` for Live Activity pushes.
|
|
142680
|
+
*
|
|
142681
|
+
* The `.push-type.liveactivity` suffix is mandatory and is why the signing key
|
|
142682
|
+
* must be Team Scoped (All Topics) — a key scoped to the bundle id alone
|
|
142683
|
+
* cannot sign this topic.
|
|
142684
|
+
*/
|
|
142685
|
+
get topic() {
|
|
142686
|
+
return `${this.creds.bundleId}.push-type.liveactivity`;
|
|
142687
|
+
}
|
|
142688
|
+
/**
|
|
142689
|
+
* Mint or reuse the provider JWT.
|
|
142690
|
+
*
|
|
142691
|
+
* ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
|
|
142692
|
+
* token older than an hour, but minting one per request is wasteful and can
|
|
142693
|
+
* trip APNs' provider-token-update throttle.
|
|
142694
|
+
*/
|
|
142695
|
+
getJwt(now = Date.now()) {
|
|
142696
|
+
const nowSeconds = Math.floor(now / 1e3);
|
|
142697
|
+
if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
|
|
142698
|
+
return this.cachedJwt.token;
|
|
142699
|
+
}
|
|
142700
|
+
const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
|
|
142701
|
+
const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
|
|
142702
|
+
const signingInput = `${header}.${payload}`;
|
|
142703
|
+
const signature = (0, import_node_crypto.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
|
|
142704
|
+
const token = `${signingInput}.${base64url3(signature)}`;
|
|
142705
|
+
this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
|
|
142706
|
+
return token;
|
|
142707
|
+
}
|
|
142708
|
+
/**
|
|
142709
|
+
* Reuse one HTTP/2 session across sends.
|
|
142710
|
+
*
|
|
142711
|
+
* APNs expects a long-lived connection; a fresh TLS handshake per push is slow
|
|
142712
|
+
* and Apple treats connection churn as abuse.
|
|
142713
|
+
*/
|
|
142714
|
+
getSession() {
|
|
142715
|
+
if (this.session && !this.session.closed && !this.session.destroyed) {
|
|
142716
|
+
return this.session;
|
|
142717
|
+
}
|
|
142718
|
+
const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
|
|
142719
|
+
session.on("error", (err) => {
|
|
142720
|
+
log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
142721
|
+
});
|
|
142722
|
+
this.session = session;
|
|
142723
|
+
return session;
|
|
142724
|
+
}
|
|
142725
|
+
/**
|
|
142726
|
+
* Send one push.
|
|
142727
|
+
*
|
|
142728
|
+
* Resolves with a result rather than rejecting on an APNs rejection: a
|
|
142729
|
+
* rejected push is an expected outcome the caller must act on (retire the
|
|
142730
|
+
* token), not an exception. Only a genuinely unexpected local failure throws,
|
|
142731
|
+
* and the caller logs it.
|
|
142732
|
+
*/
|
|
142733
|
+
async send(args) {
|
|
142734
|
+
const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
|
|
142735
|
+
if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
|
|
142736
|
+
throw new Error(
|
|
142737
|
+
`APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
|
|
142738
|
+
);
|
|
142739
|
+
}
|
|
142740
|
+
const session = this.getSession();
|
|
142741
|
+
const headers = {
|
|
142742
|
+
[import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
|
|
142743
|
+
[import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
142744
|
+
[import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
142745
|
+
"apns-push-type": "liveactivity",
|
|
142746
|
+
"apns-topic": this.topic,
|
|
142747
|
+
"apns-priority": String(args.priority ?? 10),
|
|
142748
|
+
...args.expirationSeconds != null && {
|
|
142749
|
+
"apns-expiration": String(args.expirationSeconds)
|
|
142750
|
+
},
|
|
142751
|
+
[import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
142752
|
+
[import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
142753
|
+
};
|
|
142754
|
+
return new Promise((resolve4, reject) => {
|
|
142755
|
+
const req = session.request(headers);
|
|
142756
|
+
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
142757
|
+
req.close(import_node_http2.constants.NGHTTP2_CANCEL);
|
|
142758
|
+
resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
142759
|
+
});
|
|
142760
|
+
let status = 0;
|
|
142761
|
+
req.on("response", (resHeaders) => {
|
|
142762
|
+
status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
|
|
142763
|
+
});
|
|
142764
|
+
const chunks = [];
|
|
142765
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
142766
|
+
req.on("error", reject);
|
|
142767
|
+
req.on("end", () => {
|
|
142768
|
+
const raw2 = Buffer.concat(chunks).toString("utf-8");
|
|
142769
|
+
let reason;
|
|
142770
|
+
if (raw2.length > 0) {
|
|
142771
|
+
try {
|
|
142772
|
+
reason = JSON.parse(raw2).reason;
|
|
142773
|
+
} catch {
|
|
142774
|
+
reason = raw2.slice(0, 200);
|
|
142775
|
+
}
|
|
142776
|
+
}
|
|
142777
|
+
resolve4({
|
|
142778
|
+
ok: status === 200,
|
|
142779
|
+
status,
|
|
142780
|
+
reason,
|
|
142781
|
+
tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
|
|
142782
|
+
});
|
|
142783
|
+
});
|
|
142784
|
+
req.end(body);
|
|
142785
|
+
});
|
|
142786
|
+
}
|
|
142787
|
+
/** Close the shared connection. Called on server shutdown. */
|
|
142788
|
+
close() {
|
|
142789
|
+
this.session?.close();
|
|
142790
|
+
this.session = null;
|
|
142791
|
+
}
|
|
142792
|
+
};
|
|
142793
|
+
|
|
142794
|
+
// src/api/routes/misc.routes.ts
|
|
142631
142795
|
function numberOrNull(value) {
|
|
142632
142796
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
142633
142797
|
}
|
|
@@ -142657,11 +142821,22 @@ function readRawBody3(req) {
|
|
|
142657
142821
|
function verifyWebhookSignature(body, header, secret) {
|
|
142658
142822
|
if (!header) return false;
|
|
142659
142823
|
const provided = header.startsWith("sha256=") ? header.slice(7) : header;
|
|
142660
|
-
const expected = (0,
|
|
142824
|
+
const expected = (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("hex");
|
|
142661
142825
|
const a = Buffer.from(provided, "utf-8");
|
|
142662
142826
|
const b2 = Buffer.from(expected, "utf-8");
|
|
142663
142827
|
if (a.length !== b2.length) return false;
|
|
142664
|
-
return (0,
|
|
142828
|
+
return (0, import_node_crypto2.timingSafeEqual)(a, b2);
|
|
142829
|
+
}
|
|
142830
|
+
function describePushCapability(liveActivityEnabled, env = process.env) {
|
|
142831
|
+
if (liveActivityEnabled) return { liveActivity: true, notifications: false };
|
|
142832
|
+
return {
|
|
142833
|
+
liveActivity: false,
|
|
142834
|
+
notifications: false,
|
|
142835
|
+
// describeMissingApnsCredentials only explains a *credential* gap and
|
|
142836
|
+
// returns null once the credentials are complete — reachable here, because
|
|
142837
|
+
// an unavailable token store disables the feature with the key still set.
|
|
142838
|
+
liveActivityReason: describeMissingApnsCredentials(env) ?? "APNs credentials are set but the push token store is unavailable, so Live Activity push is disabled."
|
|
142839
|
+
};
|
|
142665
142840
|
}
|
|
142666
142841
|
var clientLog = getLogger("client");
|
|
142667
142842
|
var createMiscRoutes = (deps) => {
|
|
@@ -142684,7 +142859,12 @@ var createMiscRoutes = (deps) => {
|
|
|
142684
142859
|
featureFlags: true,
|
|
142685
142860
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
142686
142861
|
// Hub's grouped views need before they can draw a tree.
|
|
142687
|
-
projectSummary: true
|
|
142862
|
+
projectSummary: true,
|
|
142863
|
+
// Delivery capability, not endpoint support: whether this server can
|
|
142864
|
+
// actually send a push, so mobile can hide an affordance instead of
|
|
142865
|
+
// registering tokens nothing will ever send to. Absent on older servers,
|
|
142866
|
+
// which a client should read as "unknown", not "unavailable".
|
|
142867
|
+
push: describePushCapability(deps.liveActivityPushEnabled())
|
|
142688
142868
|
});
|
|
142689
142869
|
});
|
|
142690
142870
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -142745,9 +142925,10 @@ var createMiscRoutes = (deps) => {
|
|
|
142745
142925
|
return c.json({ ok: true });
|
|
142746
142926
|
});
|
|
142747
142927
|
app.get("/api/push/health", (c) => {
|
|
142928
|
+
const push = describePushCapability(deps.liveActivityPushEnabled());
|
|
142748
142929
|
const repo = deps.pushRepo();
|
|
142749
|
-
if (!repo) return c.json({ tokens: [], available: false });
|
|
142750
|
-
return c.json({ tokens: repo.listHealth(), available: true });
|
|
142930
|
+
if (!repo) return c.json({ tokens: [], available: false, push });
|
|
142931
|
+
return c.json({ tokens: repo.listHealth(), available: true, push });
|
|
142751
142932
|
});
|
|
142752
142933
|
app.post("/api/__update", async (c) => {
|
|
142753
142934
|
const cfg = loadUpdateConfig();
|
|
@@ -142815,7 +142996,7 @@ var createPairRoutes = (deps) => {
|
|
|
142815
142996
|
};
|
|
142816
142997
|
|
|
142817
142998
|
// src/api/routes/progress.routes.ts
|
|
142818
|
-
var
|
|
142999
|
+
var import_node_crypto3 = __toESM(require("crypto"), 1);
|
|
142819
143000
|
function readRawBody4(req) {
|
|
142820
143001
|
return new Promise((resolve4, reject) => {
|
|
142821
143002
|
const chunks = [];
|
|
@@ -142826,10 +143007,10 @@ function readRawBody4(req) {
|
|
|
142826
143007
|
}
|
|
142827
143008
|
function verifySignature(rawBody, signature, secret) {
|
|
142828
143009
|
if (!signature || signature.length === 0) return false;
|
|
142829
|
-
const expected =
|
|
143010
|
+
const expected = import_node_crypto3.default.createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
142830
143011
|
if (expected.length !== signature.length) return false;
|
|
142831
143012
|
try {
|
|
142832
|
-
return
|
|
143013
|
+
return import_node_crypto3.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
142833
143014
|
} catch {
|
|
142834
143015
|
return false;
|
|
142835
143016
|
}
|
|
@@ -143382,7 +143563,7 @@ var import_promises13 = require("timers/promises");
|
|
|
143382
143563
|
|
|
143383
143564
|
// src/db/query-timing.ts
|
|
143384
143565
|
init_logger();
|
|
143385
|
-
var
|
|
143566
|
+
var log4 = getLogger("db");
|
|
143386
143567
|
var DEFAULT_SLOW_QUERY_MS = 35;
|
|
143387
143568
|
var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
|
|
143388
143569
|
function deriveLabel(sql) {
|
|
@@ -143398,15 +143579,15 @@ function resolveSlowMs() {
|
|
|
143398
143579
|
}
|
|
143399
143580
|
function record2(label, ms2, rows, slowMs) {
|
|
143400
143581
|
if (slowMs > 0 && ms2 >= slowMs) {
|
|
143401
|
-
|
|
143582
|
+
log4.warn(
|
|
143402
143583
|
`[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
|
|
143403
143584
|
{ event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143404
143585
|
"pino"
|
|
143405
143586
|
);
|
|
143406
143587
|
return;
|
|
143407
143588
|
}
|
|
143408
|
-
if (
|
|
143409
|
-
|
|
143589
|
+
if (log4.pino.isLevelEnabled("debug")) {
|
|
143590
|
+
log4.debug(
|
|
143410
143591
|
`[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
|
|
143411
143592
|
{ event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143412
143593
|
"pino"
|
|
@@ -146810,168 +146991,6 @@ function deriveProjectChatTitle(input) {
|
|
|
146810
146991
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
146811
146992
|
}
|
|
146812
146993
|
|
|
146813
|
-
// src/services/push/apnsClient.ts
|
|
146814
|
-
var import_node_crypto3 = require("crypto");
|
|
146815
|
-
var import_node_http2 = require("http2");
|
|
146816
|
-
init_logger();
|
|
146817
|
-
var log4 = getLogger("apns");
|
|
146818
|
-
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
146819
|
-
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
146820
|
-
var JWT_TTL_SECONDS = 3e3;
|
|
146821
|
-
var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
|
|
146822
|
-
"BadDeviceToken",
|
|
146823
|
-
"DeviceTokenNotForTopic",
|
|
146824
|
-
"Unregistered",
|
|
146825
|
-
"ExpiredToken"
|
|
146826
|
-
]);
|
|
146827
|
-
function base64url3(input) {
|
|
146828
|
-
return Buffer.from(input).toString("base64url");
|
|
146829
|
-
}
|
|
146830
|
-
function readApnsCredentialsFromEnv(env = process.env) {
|
|
146831
|
-
const key = env.APNS_KEY;
|
|
146832
|
-
if (!key || key.trim().length === 0) return null;
|
|
146833
|
-
const keyId = env.APNS_KEY_ID?.trim();
|
|
146834
|
-
const teamId = env.APNS_TEAM_ID?.trim();
|
|
146835
|
-
const bundleId = env.APNS_BUNDLE_ID?.trim();
|
|
146836
|
-
if (!keyId || !teamId || !bundleId) return null;
|
|
146837
|
-
const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
|
|
146838
|
-
return { key, keyId, teamId, bundleId, host };
|
|
146839
|
-
}
|
|
146840
|
-
function describeMissingApnsCredentials(env = process.env) {
|
|
146841
|
-
if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
|
|
146842
|
-
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.";
|
|
146843
|
-
}
|
|
146844
|
-
const missing = [
|
|
146845
|
-
["APNS_KEY_ID", env.APNS_KEY_ID],
|
|
146846
|
-
["APNS_TEAM_ID", env.APNS_TEAM_ID],
|
|
146847
|
-
["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
|
|
146848
|
-
].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
|
|
146849
|
-
if (missing.length === 0) return null;
|
|
146850
|
-
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.`;
|
|
146851
|
-
}
|
|
146852
|
-
var ApnsClient = class {
|
|
146853
|
-
constructor(creds) {
|
|
146854
|
-
this.creds = creds;
|
|
146855
|
-
}
|
|
146856
|
-
creds;
|
|
146857
|
-
session = null;
|
|
146858
|
-
cachedJwt = null;
|
|
146859
|
-
/**
|
|
146860
|
-
* The `apns-topic` for Live Activity pushes.
|
|
146861
|
-
*
|
|
146862
|
-
* The `.push-type.liveactivity` suffix is mandatory and is why the signing key
|
|
146863
|
-
* must be Team Scoped (All Topics) — a key scoped to the bundle id alone
|
|
146864
|
-
* cannot sign this topic.
|
|
146865
|
-
*/
|
|
146866
|
-
get topic() {
|
|
146867
|
-
return `${this.creds.bundleId}.push-type.liveactivity`;
|
|
146868
|
-
}
|
|
146869
|
-
/**
|
|
146870
|
-
* Mint or reuse the provider JWT.
|
|
146871
|
-
*
|
|
146872
|
-
* ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
|
|
146873
|
-
* token older than an hour, but minting one per request is wasteful and can
|
|
146874
|
-
* trip APNs' provider-token-update throttle.
|
|
146875
|
-
*/
|
|
146876
|
-
getJwt(now = Date.now()) {
|
|
146877
|
-
const nowSeconds = Math.floor(now / 1e3);
|
|
146878
|
-
if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
|
|
146879
|
-
return this.cachedJwt.token;
|
|
146880
|
-
}
|
|
146881
|
-
const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
|
|
146882
|
-
const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
|
|
146883
|
-
const signingInput = `${header}.${payload}`;
|
|
146884
|
-
const signature = (0, import_node_crypto3.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
|
|
146885
|
-
const token = `${signingInput}.${base64url3(signature)}`;
|
|
146886
|
-
this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
|
|
146887
|
-
return token;
|
|
146888
|
-
}
|
|
146889
|
-
/**
|
|
146890
|
-
* Reuse one HTTP/2 session across sends.
|
|
146891
|
-
*
|
|
146892
|
-
* APNs expects a long-lived connection; a fresh TLS handshake per push is slow
|
|
146893
|
-
* and Apple treats connection churn as abuse.
|
|
146894
|
-
*/
|
|
146895
|
-
getSession() {
|
|
146896
|
-
if (this.session && !this.session.closed && !this.session.destroyed) {
|
|
146897
|
-
return this.session;
|
|
146898
|
-
}
|
|
146899
|
-
const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
|
|
146900
|
-
session.on("error", (err) => {
|
|
146901
|
-
log4.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
146902
|
-
});
|
|
146903
|
-
this.session = session;
|
|
146904
|
-
return session;
|
|
146905
|
-
}
|
|
146906
|
-
/**
|
|
146907
|
-
* Send one push.
|
|
146908
|
-
*
|
|
146909
|
-
* Resolves with a result rather than rejecting on an APNs rejection: a
|
|
146910
|
-
* rejected push is an expected outcome the caller must act on (retire the
|
|
146911
|
-
* token), not an exception. Only a genuinely unexpected local failure throws,
|
|
146912
|
-
* and the caller logs it.
|
|
146913
|
-
*/
|
|
146914
|
-
async send(args) {
|
|
146915
|
-
const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
|
|
146916
|
-
if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
|
|
146917
|
-
throw new Error(
|
|
146918
|
-
`APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
|
|
146919
|
-
);
|
|
146920
|
-
}
|
|
146921
|
-
const session = this.getSession();
|
|
146922
|
-
const headers = {
|
|
146923
|
-
[import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
|
|
146924
|
-
[import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
146925
|
-
[import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
146926
|
-
"apns-push-type": "liveactivity",
|
|
146927
|
-
"apns-topic": this.topic,
|
|
146928
|
-
"apns-priority": String(args.priority ?? 10),
|
|
146929
|
-
...args.expirationSeconds != null && {
|
|
146930
|
-
"apns-expiration": String(args.expirationSeconds)
|
|
146931
|
-
},
|
|
146932
|
-
[import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
146933
|
-
[import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
146934
|
-
};
|
|
146935
|
-
return new Promise((resolve4, reject) => {
|
|
146936
|
-
const req = session.request(headers);
|
|
146937
|
-
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
146938
|
-
req.close(import_node_http2.constants.NGHTTP2_CANCEL);
|
|
146939
|
-
resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
146940
|
-
});
|
|
146941
|
-
let status = 0;
|
|
146942
|
-
req.on("response", (resHeaders) => {
|
|
146943
|
-
status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
|
|
146944
|
-
});
|
|
146945
|
-
const chunks = [];
|
|
146946
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
146947
|
-
req.on("error", reject);
|
|
146948
|
-
req.on("end", () => {
|
|
146949
|
-
const raw2 = Buffer.concat(chunks).toString("utf-8");
|
|
146950
|
-
let reason;
|
|
146951
|
-
if (raw2.length > 0) {
|
|
146952
|
-
try {
|
|
146953
|
-
reason = JSON.parse(raw2).reason;
|
|
146954
|
-
} catch {
|
|
146955
|
-
reason = raw2.slice(0, 200);
|
|
146956
|
-
}
|
|
146957
|
-
}
|
|
146958
|
-
resolve4({
|
|
146959
|
-
ok: status === 200,
|
|
146960
|
-
status,
|
|
146961
|
-
reason,
|
|
146962
|
-
tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
|
|
146963
|
-
});
|
|
146964
|
-
});
|
|
146965
|
-
req.end(body);
|
|
146966
|
-
});
|
|
146967
|
-
}
|
|
146968
|
-
/** Close the shared connection. Called on server shutdown. */
|
|
146969
|
-
close() {
|
|
146970
|
-
this.session?.close();
|
|
146971
|
-
this.session = null;
|
|
146972
|
-
}
|
|
146973
|
-
};
|
|
146974
|
-
|
|
146975
146994
|
// src/services/push/expoPushSender.ts
|
|
146976
146995
|
init_logger();
|
|
146977
146996
|
var log5 = getLogger("expo-push");
|
|
@@ -149290,6 +149309,7 @@ var StreamerServer = class {
|
|
|
149290
149309
|
cache: () => this.cache,
|
|
149291
149310
|
cacheMonitor: () => this.cacheMonitor,
|
|
149292
149311
|
pushRepo: () => this.pushRepo,
|
|
149312
|
+
liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
|
|
149293
149313
|
devicesRepo: () => this.devicesRepo,
|
|
149294
149314
|
projectsRepo: () => this.projectsRepo,
|
|
149295
149315
|
conversationsRepo: () => this.conversationsRepo,
|