KI-AGENT: APNs Private Key im Push Server normalisieren
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import http2 from "node:http2";
|
||||
import { createPrivateKey, type KeyObject } from "node:crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { env } from "../config/env.js";
|
||||
|
||||
@@ -21,6 +22,9 @@ export type ApnsResult =
|
||||
| { ok: false; code: string; message: string; permanent: boolean };
|
||||
|
||||
export class ApnsClient {
|
||||
private signingKey?: KeyObject;
|
||||
private signingKeyError?: string;
|
||||
|
||||
isConfigured(): boolean {
|
||||
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 };
|
||||
}
|
||||
|
||||
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 token = jwt.sign(
|
||||
{ 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 } },
|
||||
);
|
||||
|
||||
@@ -88,6 +102,23 @@ export class ApnsClient {
|
||||
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 {
|
||||
@@ -98,3 +129,15 @@ function safeReason(body: string): string | 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]}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user