@threadbase-sh/streamer 1.48.0 → 1.50.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 +210 -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
|
@@ -1490,6 +1490,10 @@ function isPermissionMode(value) {
|
|
|
1490
1490
|
function isDangerousPermissionMode(mode) {
|
|
1491
1491
|
return DANGEROUS_PERMISSION_MODES.includes(mode);
|
|
1492
1492
|
}
|
|
1493
|
+
function effectivePermissionMode(flags, fallback) {
|
|
1494
|
+
const candidate = flags?.permissionMode ?? fallback;
|
|
1495
|
+
return isPermissionMode(candidate) ? candidate : void 0;
|
|
1496
|
+
}
|
|
1493
1497
|
function isEffortLevel(value) {
|
|
1494
1498
|
return typeof value === "string" && EFFORT_LEVELS.includes(value);
|
|
1495
1499
|
}
|
|
@@ -142369,7 +142373,7 @@ function createLogsRoutes() {
|
|
|
142369
142373
|
|
|
142370
142374
|
// src/api/routes/misc.routes.ts
|
|
142371
142375
|
var import_node_child_process2 = require("child_process");
|
|
142372
|
-
var
|
|
142376
|
+
var import_node_crypto2 = require("crypto");
|
|
142373
142377
|
var import_os7 = require("os");
|
|
142374
142378
|
|
|
142375
142379
|
// src/db/repositories/push.repository.ts
|
|
@@ -142628,6 +142632,170 @@ var PushRepository = class {
|
|
|
142628
142632
|
|
|
142629
142633
|
// src/api/routes/misc.routes.ts
|
|
142630
142634
|
init_logger();
|
|
142635
|
+
|
|
142636
|
+
// src/services/push/apnsClient.ts
|
|
142637
|
+
var import_node_crypto = require("crypto");
|
|
142638
|
+
var import_node_http2 = require("http2");
|
|
142639
|
+
init_logger();
|
|
142640
|
+
var log3 = getLogger("apns");
|
|
142641
|
+
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
142642
|
+
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
142643
|
+
var JWT_TTL_SECONDS = 3e3;
|
|
142644
|
+
var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
|
|
142645
|
+
"BadDeviceToken",
|
|
142646
|
+
"DeviceTokenNotForTopic",
|
|
142647
|
+
"Unregistered",
|
|
142648
|
+
"ExpiredToken"
|
|
142649
|
+
]);
|
|
142650
|
+
function base64url3(input) {
|
|
142651
|
+
return Buffer.from(input).toString("base64url");
|
|
142652
|
+
}
|
|
142653
|
+
function readApnsCredentialsFromEnv(env = process.env) {
|
|
142654
|
+
const key = env.APNS_KEY;
|
|
142655
|
+
if (!key || key.trim().length === 0) return null;
|
|
142656
|
+
const keyId = env.APNS_KEY_ID?.trim();
|
|
142657
|
+
const teamId = env.APNS_TEAM_ID?.trim();
|
|
142658
|
+
const bundleId = env.APNS_BUNDLE_ID?.trim();
|
|
142659
|
+
if (!keyId || !teamId || !bundleId) return null;
|
|
142660
|
+
const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
|
|
142661
|
+
return { key, keyId, teamId, bundleId, host };
|
|
142662
|
+
}
|
|
142663
|
+
function describeMissingApnsCredentials(env = process.env) {
|
|
142664
|
+
if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
|
|
142665
|
+
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.";
|
|
142666
|
+
}
|
|
142667
|
+
const missing = [
|
|
142668
|
+
["APNS_KEY_ID", env.APNS_KEY_ID],
|
|
142669
|
+
["APNS_TEAM_ID", env.APNS_TEAM_ID],
|
|
142670
|
+
["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
|
|
142671
|
+
].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
|
|
142672
|
+
if (missing.length === 0) return null;
|
|
142673
|
+
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.`;
|
|
142674
|
+
}
|
|
142675
|
+
var ApnsClient = class {
|
|
142676
|
+
constructor(creds) {
|
|
142677
|
+
this.creds = creds;
|
|
142678
|
+
}
|
|
142679
|
+
creds;
|
|
142680
|
+
session = null;
|
|
142681
|
+
cachedJwt = null;
|
|
142682
|
+
/**
|
|
142683
|
+
* The `apns-topic` for Live Activity pushes.
|
|
142684
|
+
*
|
|
142685
|
+
* The `.push-type.liveactivity` suffix is mandatory and is why the signing key
|
|
142686
|
+
* must be Team Scoped (All Topics) — a key scoped to the bundle id alone
|
|
142687
|
+
* cannot sign this topic.
|
|
142688
|
+
*/
|
|
142689
|
+
get topic() {
|
|
142690
|
+
return `${this.creds.bundleId}.push-type.liveactivity`;
|
|
142691
|
+
}
|
|
142692
|
+
/**
|
|
142693
|
+
* Mint or reuse the provider JWT.
|
|
142694
|
+
*
|
|
142695
|
+
* ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
|
|
142696
|
+
* token older than an hour, but minting one per request is wasteful and can
|
|
142697
|
+
* trip APNs' provider-token-update throttle.
|
|
142698
|
+
*/
|
|
142699
|
+
getJwt(now = Date.now()) {
|
|
142700
|
+
const nowSeconds = Math.floor(now / 1e3);
|
|
142701
|
+
if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
|
|
142702
|
+
return this.cachedJwt.token;
|
|
142703
|
+
}
|
|
142704
|
+
const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
|
|
142705
|
+
const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
|
|
142706
|
+
const signingInput = `${header}.${payload}`;
|
|
142707
|
+
const signature = (0, import_node_crypto.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
|
|
142708
|
+
const token = `${signingInput}.${base64url3(signature)}`;
|
|
142709
|
+
this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
|
|
142710
|
+
return token;
|
|
142711
|
+
}
|
|
142712
|
+
/**
|
|
142713
|
+
* Reuse one HTTP/2 session across sends.
|
|
142714
|
+
*
|
|
142715
|
+
* APNs expects a long-lived connection; a fresh TLS handshake per push is slow
|
|
142716
|
+
* and Apple treats connection churn as abuse.
|
|
142717
|
+
*/
|
|
142718
|
+
getSession() {
|
|
142719
|
+
if (this.session && !this.session.closed && !this.session.destroyed) {
|
|
142720
|
+
return this.session;
|
|
142721
|
+
}
|
|
142722
|
+
const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
|
|
142723
|
+
session.on("error", (err) => {
|
|
142724
|
+
log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
142725
|
+
});
|
|
142726
|
+
this.session = session;
|
|
142727
|
+
return session;
|
|
142728
|
+
}
|
|
142729
|
+
/**
|
|
142730
|
+
* Send one push.
|
|
142731
|
+
*
|
|
142732
|
+
* Resolves with a result rather than rejecting on an APNs rejection: a
|
|
142733
|
+
* rejected push is an expected outcome the caller must act on (retire the
|
|
142734
|
+
* token), not an exception. Only a genuinely unexpected local failure throws,
|
|
142735
|
+
* and the caller logs it.
|
|
142736
|
+
*/
|
|
142737
|
+
async send(args) {
|
|
142738
|
+
const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
|
|
142739
|
+
if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
|
|
142740
|
+
throw new Error(
|
|
142741
|
+
`APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
|
|
142742
|
+
);
|
|
142743
|
+
}
|
|
142744
|
+
const session = this.getSession();
|
|
142745
|
+
const headers = {
|
|
142746
|
+
[import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
|
|
142747
|
+
[import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
142748
|
+
[import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
142749
|
+
"apns-push-type": "liveactivity",
|
|
142750
|
+
"apns-topic": this.topic,
|
|
142751
|
+
"apns-priority": String(args.priority ?? 10),
|
|
142752
|
+
...args.expirationSeconds != null && {
|
|
142753
|
+
"apns-expiration": String(args.expirationSeconds)
|
|
142754
|
+
},
|
|
142755
|
+
[import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
142756
|
+
[import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
142757
|
+
};
|
|
142758
|
+
return new Promise((resolve4, reject) => {
|
|
142759
|
+
const req = session.request(headers);
|
|
142760
|
+
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
142761
|
+
req.close(import_node_http2.constants.NGHTTP2_CANCEL);
|
|
142762
|
+
resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
142763
|
+
});
|
|
142764
|
+
let status = 0;
|
|
142765
|
+
req.on("response", (resHeaders) => {
|
|
142766
|
+
status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
|
|
142767
|
+
});
|
|
142768
|
+
const chunks = [];
|
|
142769
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
142770
|
+
req.on("error", reject);
|
|
142771
|
+
req.on("end", () => {
|
|
142772
|
+
const raw2 = Buffer.concat(chunks).toString("utf-8");
|
|
142773
|
+
let reason;
|
|
142774
|
+
if (raw2.length > 0) {
|
|
142775
|
+
try {
|
|
142776
|
+
reason = JSON.parse(raw2).reason;
|
|
142777
|
+
} catch {
|
|
142778
|
+
reason = raw2.slice(0, 200);
|
|
142779
|
+
}
|
|
142780
|
+
}
|
|
142781
|
+
resolve4({
|
|
142782
|
+
ok: status === 200,
|
|
142783
|
+
status,
|
|
142784
|
+
reason,
|
|
142785
|
+
tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
|
|
142786
|
+
});
|
|
142787
|
+
});
|
|
142788
|
+
req.end(body);
|
|
142789
|
+
});
|
|
142790
|
+
}
|
|
142791
|
+
/** Close the shared connection. Called on server shutdown. */
|
|
142792
|
+
close() {
|
|
142793
|
+
this.session?.close();
|
|
142794
|
+
this.session = null;
|
|
142795
|
+
}
|
|
142796
|
+
};
|
|
142797
|
+
|
|
142798
|
+
// src/api/routes/misc.routes.ts
|
|
142631
142799
|
function numberOrNull(value) {
|
|
142632
142800
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
142633
142801
|
}
|
|
@@ -142657,11 +142825,22 @@ function readRawBody3(req) {
|
|
|
142657
142825
|
function verifyWebhookSignature(body, header, secret) {
|
|
142658
142826
|
if (!header) return false;
|
|
142659
142827
|
const provided = header.startsWith("sha256=") ? header.slice(7) : header;
|
|
142660
|
-
const expected = (0,
|
|
142828
|
+
const expected = (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("hex");
|
|
142661
142829
|
const a = Buffer.from(provided, "utf-8");
|
|
142662
142830
|
const b2 = Buffer.from(expected, "utf-8");
|
|
142663
142831
|
if (a.length !== b2.length) return false;
|
|
142664
|
-
return (0,
|
|
142832
|
+
return (0, import_node_crypto2.timingSafeEqual)(a, b2);
|
|
142833
|
+
}
|
|
142834
|
+
function describePushCapability(liveActivityEnabled, env = process.env) {
|
|
142835
|
+
if (liveActivityEnabled) return { liveActivity: true, notifications: false };
|
|
142836
|
+
return {
|
|
142837
|
+
liveActivity: false,
|
|
142838
|
+
notifications: false,
|
|
142839
|
+
// describeMissingApnsCredentials only explains a *credential* gap and
|
|
142840
|
+
// returns null once the credentials are complete — reachable here, because
|
|
142841
|
+
// an unavailable token store disables the feature with the key still set.
|
|
142842
|
+
liveActivityReason: describeMissingApnsCredentials(env) ?? "APNs credentials are set but the push token store is unavailable, so Live Activity push is disabled."
|
|
142843
|
+
};
|
|
142665
142844
|
}
|
|
142666
142845
|
var clientLog = getLogger("client");
|
|
142667
142846
|
var createMiscRoutes = (deps) => {
|
|
@@ -142684,7 +142863,12 @@ var createMiscRoutes = (deps) => {
|
|
|
142684
142863
|
featureFlags: true,
|
|
142685
142864
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
142686
142865
|
// Hub's grouped views need before they can draw a tree.
|
|
142687
|
-
projectSummary: true
|
|
142866
|
+
projectSummary: true,
|
|
142867
|
+
// Delivery capability, not endpoint support: whether this server can
|
|
142868
|
+
// actually send a push, so mobile can hide an affordance instead of
|
|
142869
|
+
// registering tokens nothing will ever send to. Absent on older servers,
|
|
142870
|
+
// which a client should read as "unknown", not "unavailable".
|
|
142871
|
+
push: describePushCapability(deps.liveActivityPushEnabled())
|
|
142688
142872
|
});
|
|
142689
142873
|
});
|
|
142690
142874
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -142745,9 +142929,10 @@ var createMiscRoutes = (deps) => {
|
|
|
142745
142929
|
return c.json({ ok: true });
|
|
142746
142930
|
});
|
|
142747
142931
|
app.get("/api/push/health", (c) => {
|
|
142932
|
+
const push = describePushCapability(deps.liveActivityPushEnabled());
|
|
142748
142933
|
const repo = deps.pushRepo();
|
|
142749
|
-
if (!repo) return c.json({ tokens: [], available: false });
|
|
142750
|
-
return c.json({ tokens: repo.listHealth(), available: true });
|
|
142934
|
+
if (!repo) return c.json({ tokens: [], available: false, push });
|
|
142935
|
+
return c.json({ tokens: repo.listHealth(), available: true, push });
|
|
142751
142936
|
});
|
|
142752
142937
|
app.post("/api/__update", async (c) => {
|
|
142753
142938
|
const cfg = loadUpdateConfig();
|
|
@@ -142815,7 +143000,7 @@ var createPairRoutes = (deps) => {
|
|
|
142815
143000
|
};
|
|
142816
143001
|
|
|
142817
143002
|
// src/api/routes/progress.routes.ts
|
|
142818
|
-
var
|
|
143003
|
+
var import_node_crypto3 = __toESM(require("crypto"), 1);
|
|
142819
143004
|
function readRawBody4(req) {
|
|
142820
143005
|
return new Promise((resolve4, reject) => {
|
|
142821
143006
|
const chunks = [];
|
|
@@ -142826,10 +143011,10 @@ function readRawBody4(req) {
|
|
|
142826
143011
|
}
|
|
142827
143012
|
function verifySignature(rawBody, signature, secret) {
|
|
142828
143013
|
if (!signature || signature.length === 0) return false;
|
|
142829
|
-
const expected =
|
|
143014
|
+
const expected = import_node_crypto3.default.createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
142830
143015
|
if (expected.length !== signature.length) return false;
|
|
142831
143016
|
try {
|
|
142832
|
-
return
|
|
143017
|
+
return import_node_crypto3.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
142833
143018
|
} catch {
|
|
142834
143019
|
return false;
|
|
142835
143020
|
}
|
|
@@ -143382,7 +143567,7 @@ var import_promises13 = require("timers/promises");
|
|
|
143382
143567
|
|
|
143383
143568
|
// src/db/query-timing.ts
|
|
143384
143569
|
init_logger();
|
|
143385
|
-
var
|
|
143570
|
+
var log4 = getLogger("db");
|
|
143386
143571
|
var DEFAULT_SLOW_QUERY_MS = 35;
|
|
143387
143572
|
var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
|
|
143388
143573
|
function deriveLabel(sql) {
|
|
@@ -143398,15 +143583,15 @@ function resolveSlowMs() {
|
|
|
143398
143583
|
}
|
|
143399
143584
|
function record2(label, ms2, rows, slowMs) {
|
|
143400
143585
|
if (slowMs > 0 && ms2 >= slowMs) {
|
|
143401
|
-
|
|
143586
|
+
log4.warn(
|
|
143402
143587
|
`[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
|
|
143403
143588
|
{ event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143404
143589
|
"pino"
|
|
143405
143590
|
);
|
|
143406
143591
|
return;
|
|
143407
143592
|
}
|
|
143408
|
-
if (
|
|
143409
|
-
|
|
143593
|
+
if (log4.pino.isLevelEnabled("debug")) {
|
|
143594
|
+
log4.debug(
|
|
143410
143595
|
`[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
|
|
143411
143596
|
{ event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143412
143597
|
"pino"
|
|
@@ -146810,168 +146995,6 @@ function deriveProjectChatTitle(input) {
|
|
|
146810
146995
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
146811
146996
|
}
|
|
146812
146997
|
|
|
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
146998
|
// src/services/push/expoPushSender.ts
|
|
146976
146999
|
init_logger();
|
|
146977
147000
|
var log5 = getLogger("expo-push");
|
|
@@ -149290,6 +149313,7 @@ var StreamerServer = class {
|
|
|
149290
149313
|
cache: () => this.cache,
|
|
149291
149314
|
cacheMonitor: () => this.cacheMonitor,
|
|
149292
149315
|
pushRepo: () => this.pushRepo,
|
|
149316
|
+
liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
|
|
149293
149317
|
devicesRepo: () => this.devicesRepo,
|
|
149294
149318
|
projectsRepo: () => this.projectsRepo,
|
|
149295
149319
|
conversationsRepo: () => this.conversationsRepo,
|
|
@@ -157454,6 +157478,17 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
157454
157478
|
takeoverProd2({ port: plan.port, repoToplevel });
|
|
157455
157479
|
}
|
|
157456
157480
|
}
|
|
157481
|
+
const spawnMode = effectivePermissionMode(
|
|
157482
|
+
claudeFlags,
|
|
157483
|
+
resolvedDefaultPermissionMode ?? loadDefaultPermissionMode()
|
|
157484
|
+
);
|
|
157485
|
+
if (spawnMode !== void 0 && isDangerousPermissionMode(spawnMode)) {
|
|
157486
|
+
log14.warn(
|
|
157487
|
+
`[WARN] permission mode is ${spawnMode} \u2014 spawned sessions run without confirmation prompts, so anyone holding the API key can execute arbitrary code on this machine. There is no spend limit.`,
|
|
157488
|
+
void 0,
|
|
157489
|
+
"console"
|
|
157490
|
+
);
|
|
157491
|
+
}
|
|
157457
157492
|
const server = new StreamerServer({
|
|
157458
157493
|
port: resolvedPort,
|
|
157459
157494
|
apiKey,
|