KI-AGENT: APNs Private Key im Push Server normalisieren
This commit is contained in:
@@ -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]}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user