KI-AGENT: APNs Private Key im Push Server normalisieren

This commit is contained in:
2026-05-22 18:28:52 +02:00
parent 4bcc2152ab
commit d73209a150

View File

@@ -1,4 +1,5 @@
import http2 from "node:http2"; import http2 from "node:http2";
import { createPrivateKey, type KeyObject } from "node:crypto";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { env } from "../config/env.js"; import { env } from "../config/env.js";
@@ -21,6 +22,9 @@ export type ApnsResult =
| { ok: false; code: string; message: string; permanent: boolean }; | { ok: false; code: string; message: string; permanent: boolean };
export class ApnsClient { export class ApnsClient {
private signingKey?: KeyObject;
private signingKeyError?: string;
isConfigured(): boolean { isConfigured(): boolean {
return Boolean(env.APNS_TEAM_ID && env.APNS_KEY_ID && env.APNS_PRIVATE_KEY && env.IOS_BUNDLE_ID); return Boolean(env.APNS_TEAM_ID && env.APNS_KEY_ID && env.APNS_PRIVATE_KEY && env.IOS_BUNDLE_ID);
} }
@@ -30,10 +34,20 @@ export class ApnsClient {
return { ok: false, code: "apns_not_configured", message: "APNs ist nicht vollständig konfiguriert.", permanent: false }; return { ok: false, code: "apns_not_configured", message: "APNs ist nicht vollständig konfiguriert.", permanent: false };
} }
const signingKey = this.getSigningKey();
if (!signingKey) {
return {
ok: false,
code: "apns_invalid_private_key",
message: this.signingKeyError || "APNs Private Key ist ungültig.",
permanent: false,
};
}
const host = env.APNS_PRODUCTION ? "https://api.push.apple.com" : "https://api.sandbox.push.apple.com"; const host = env.APNS_PRODUCTION ? "https://api.push.apple.com" : "https://api.sandbox.push.apple.com";
const token = jwt.sign( const token = jwt.sign(
{ iss: env.APNS_TEAM_ID, iat: Math.floor(Date.now() / 1000) }, { iss: env.APNS_TEAM_ID, iat: Math.floor(Date.now() / 1000) },
env.APNS_PRIVATE_KEY.replace(/\\n/g, "\n"), signingKey,
{ algorithm: "ES256", header: { alg: "ES256", kid: env.APNS_KEY_ID } }, { algorithm: "ES256", header: { alg: "ES256", kid: env.APNS_KEY_ID } },
); );
@@ -88,6 +102,23 @@ export class ApnsClient {
req.end(JSON.stringify(payload)); req.end(JSON.stringify(payload));
}); });
} }
private getSigningKey(): KeyObject | null {
if (this.signingKey) return this.signingKey;
try {
this.signingKey = createPrivateKey(normalizePem(env.APNS_PRIVATE_KEY));
if (this.signingKey.asymmetricKeyType !== "ec") {
this.signingKeyError = "APNs Private Key muss ein elliptic-curve Private Key aus App Store Connect sein.";
this.signingKey = undefined;
return null;
}
return this.signingKey;
} catch {
this.signingKeyError = "APNs Private Key konnte nicht gelesen werden. Bitte den .p8 Key mit Zeilenumbrüchen oder escaped \\n hinterlegen.";
return null;
}
}
} }
function safeReason(body: string): string | null { function safeReason(body: string): string | null {
@@ -98,3 +129,15 @@ function safeReason(body: string): string | null {
return null; return null;
} }
} }
function normalizePem(value: string): string {
const withEscapedNewlines = value.trim().replace(/\\n/g, "\n");
if (withEscapedNewlines.includes("\n")) return withEscapedNewlines;
const match = withEscapedNewlines.match(/^(-----BEGIN [^-]+-----)(.+)(-----END [^-]+-----)$/);
if (!match) return withEscapedNewlines;
const body = match[2].replace(/\s+/g, "");
const wrappedBody = body.match(/.{1,64}/g)?.join("\n") || body;
return `${match[1]}\n${wrappedBody}\n${match[3]}`;
}