Wire Forge-native release infrastructure
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s

This commit is contained in:
Conrad Kramer 2026-06-07 05:51:12 -07:00
parent 97c569fb35
commit 002bd382e9
199 changed files with 14268 additions and 185 deletions

View file

@ -0,0 +1,164 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { webcrypto } from "node:crypto";
const crypto = globalThis.crypto ?? webcrypto;
function b64url(input) {
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
return buf
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function parseArgs(argv) {
const out = { _: [] };
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") out.help = true;
else if (arg.startsWith("--")) {
const key = arg.slice(2);
const value = argv[i + 1];
if (!value || value.startsWith("--")) throw new Error(`missing value for --${key}`);
out[key] = value;
i++;
} else {
out._.push(arg);
}
}
return out;
}
async function makeJwt({ keyId, issuerId, keyPem }) {
const header = { alg: "ES256", kid: keyId, typ: "JWT" };
const now = Math.floor(Date.now() / 1000);
const payload = { iss: issuerId, exp: now + 20 * 60, aud: "appstoreconnect-v1" };
const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
const pkcs8Der = Buffer.from(
keyPem.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\s+/g, ""),
"base64",
);
const key = await crypto.subtle.importKey(
"pkcs8",
pkcs8Der,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
key,
new TextEncoder().encode(input),
);
return `${input}.${b64url(Buffer.from(sig))}`;
}
async function ascFetch(jwt, url, opts = {}) {
const res = await fetch(url, {
...opts,
headers: {
...(opts.headers ?? {}),
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
},
});
const text = await res.text();
let body;
try {
body = text ? JSON.parse(text) : {};
} catch {
body = { raw: text };
}
if (!res.ok) {
throw new Error(`ASC ${res.status} ${url}: ${JSON.stringify(body).slice(0, 2000)}`);
}
return body;
}
function certificateFileName(type, id) {
return `${type.toLowerCase().replace(/_/g, "-")}-${id}.cer`;
}
function csrContentForApi(csrFile) {
const bytes = fs.readFileSync(csrFile);
const text = bytes.toString("ascii");
const pemMatch = text.match(/-----BEGIN CERTIFICATE REQUEST-----([\s\S]+?)-----END CERTIFICATE REQUEST-----/);
if (pemMatch) {
return pemMatch[1].replace(/\s+/g, "");
}
return bytes.toString("base64");
}
async function main() {
const args = parseArgs(process.argv);
if (args.help) {
console.log("Usage: create-asc-certificate.mjs --key-id ID --issuer-id ID --key-file AuthKey.p8 --certificate-type IOS_DISTRIBUTION --csr-file file.csr --out-dir DIR");
process.exit(0);
}
const keyId = args["key-id"] ?? process.env.APP_STORE_CONNECT_KEY_ID;
const issuerId = args["issuer-id"] ?? process.env.APP_STORE_CONNECT_ISSUER_ID;
const keyFile = args["key-file"] ?? process.env.APP_STORE_CONNECT_KEY_FILE;
const certificateType = args["certificate-type"] ?? "IOS_DISTRIBUTION";
const csrFile = args["csr-file"];
const outDir = args["out-dir"] ?? ".";
const outFile = args["out-file"];
const metadataFile = args["metadata-file"];
if (!keyId || !issuerId || !keyFile || !csrFile) {
throw new Error("required: --key-id --issuer-id --key-file --csr-file");
}
const jwt = await makeJwt({
keyId,
issuerId,
keyPem: fs.readFileSync(keyFile, "utf8"),
});
const csrContent = csrContentForApi(csrFile);
const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/certificates", {
method: "POST",
body: JSON.stringify({
data: {
type: "certificates",
attributes: {
certificateType,
csrContent,
},
},
}),
});
const cert = created?.data;
const id = cert?.id;
const content = cert?.attributes?.certificateContent;
if (!id || !content) {
throw new Error(`ASC response did not include certificate id/content: ${JSON.stringify(created).slice(0, 2000)}`);
}
fs.mkdirSync(outDir, { recursive: true });
const certificatePath = outFile ?? path.join(outDir, certificateFileName(certificateType, id));
fs.writeFileSync(certificatePath, Buffer.from(content, "base64"));
const metadata = {
id,
certificateType: cert?.attributes?.certificateType ?? certificateType,
displayName: cert?.attributes?.displayName,
name: cert?.attributes?.name,
platform: cert?.attributes?.platform,
expirationDate: cert?.attributes?.expirationDate,
serialNumber: cert?.attributes?.serialNumber,
certificatePath,
};
if (metadataFile) {
fs.writeFileSync(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
}
process.stdout.write(`${JSON.stringify(metadata, null, 2)}\n`);
}
main().catch((err) => {
console.error(err?.stack || String(err));
process.exit(1);
});

249
Scripts/apple/google-kms-csr.py Executable file
View file

@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Build an Apple-compatible PKCS#10 CSR backed by Google Cloud KMS.
The script intentionally implements only the RSA/SHA-256 subset Burrow needs for
Apple Developer ID and iOS Distribution certificate requests. It fetches the KMS
public key, builds CertificationRequestInfo locally, asks KMS to sign that DER
payload, and writes a standard PEM CSR.
"""
from __future__ import annotations
import argparse
import base64
import os
import subprocess
import sys
import tempfile
from pathlib import Path
DEFAULT_PROJECT = "project-88c23ce9-918a-470a-b33"
DEFAULT_LOCATION = "global"
DEFAULT_KEYRING = "burrow-identity"
DEFAULT_KEY = "apple-developer-id-application"
DEFAULT_VERSION = "1"
DEFAULT_COMMON_NAME = "Burrow Developer ID Application"
DEFAULT_EMAIL = "release@burrow.net"
def der_length(length: int) -> bytes:
if length < 0:
raise ValueError("negative DER length")
if length < 0x80:
return bytes([length])
encoded = length.to_bytes((length.bit_length() + 7) // 8, "big")
return bytes([0x80 | len(encoded)]) + encoded
def der(tag: int, body: bytes) -> bytes:
return bytes([tag]) + der_length(len(body)) + body
def der_sequence(*items: bytes) -> bytes:
return der(0x30, b"".join(items))
def der_set(*items: bytes) -> bytes:
return der(0x31, b"".join(items))
def der_integer(value: int) -> bytes:
if value < 0:
raise ValueError("negative INTEGER is not supported")
raw = value.to_bytes(max(1, (value.bit_length() + 7) // 8), "big")
if raw[0] & 0x80:
raw = b"\x00" + raw
return der(0x02, raw)
def der_null() -> bytes:
return der(0x05, b"")
def der_bit_string(value: bytes) -> bytes:
return der(0x03, b"\x00" + value)
def der_utf8(value: str) -> bytes:
return der(0x0C, value.encode("utf-8"))
def der_printable(value: str) -> bytes:
return der(0x13, value.encode("ascii"))
def der_ia5(value: str) -> bytes:
return der(0x16, value.encode("ascii"))
def der_oid(dotted: str) -> bytes:
parts = [int(part) for part in dotted.split(".")]
if len(parts) < 2 or parts[0] > 2 or parts[1] > 39:
raise ValueError(f"unsupported OID: {dotted}")
body = bytes([40 * parts[0] + parts[1]])
for value in parts[2:]:
if value < 0:
raise ValueError(f"negative OID component: {dotted}")
encoded = [value & 0x7F]
value >>= 7
while value:
encoded.append(0x80 | (value & 0x7F))
value >>= 7
body += bytes(reversed(encoded))
return der(0x06, body)
def name_attribute(oid: str, value_der: bytes) -> bytes:
return der_set(der_sequence(der_oid(oid), value_der))
def subject_name(common_name: str, email_address: str, country: str | None) -> bytes:
attrs: list[bytes] = []
if country:
if len(country) != 2 or not country.isalpha():
raise ValueError("--country must be a two-letter ISO country code")
attrs.append(name_attribute("2.5.4.6", der_printable(country.upper())))
if email_address:
attrs.append(name_attribute("1.2.840.113549.1.9.1", der_ia5(email_address)))
attrs.append(name_attribute("2.5.4.3", der_utf8(common_name)))
return der_sequence(*attrs)
def pem_to_der(pem: bytes) -> bytes:
lines = [
line.strip()
for line in pem.splitlines()
if line and not line.startswith(b"-----")
]
if not lines:
raise ValueError("PEM input did not contain base64 data")
return base64.b64decode(b"".join(lines), validate=True)
def pem_wrap(label: str, body: bytes) -> str:
encoded = base64.b64encode(body).decode("ascii")
lines = [f"-----BEGIN {label}-----"]
lines.extend(encoded[i : i + 64] for i in range(0, len(encoded), 64))
lines.append(f"-----END {label}-----")
return "\n".join(lines) + "\n"
def run(command: list[str]) -> None:
subprocess.run(command, check=True)
def fetch_public_key(args: argparse.Namespace, output: Path) -> None:
command = [
args.gcloud,
"kms",
"keys",
"versions",
"get-public-key",
args.kms_version,
"--location",
args.kms_location,
"--keyring",
args.kms_keyring,
"--key",
args.kms_key,
"--output-file",
str(output),
]
if args.gcloud_project:
command.extend(["--project", args.gcloud_project])
run(command)
def kms_sign(args: argparse.Namespace, payload: bytes) -> bytes:
with tempfile.TemporaryDirectory(prefix="burrow-kms-csr.") as tmp:
payload_path = Path(tmp) / "certification-request-info.der"
signature_path = Path(tmp) / "signature.b64"
payload_path.write_bytes(payload)
command = [
args.gcloud,
"kms",
"asymmetric-sign",
"--location",
args.kms_location,
"--keyring",
args.kms_keyring,
"--key",
args.kms_key,
"--version",
args.kms_version,
"--digest-algorithm",
"sha256",
"--input-file",
str(payload_path),
"--signature-file",
str(signature_path),
]
if args.gcloud_project:
command.extend(["--project", args.gcloud_project])
run(command)
signature = signature_path.read_bytes()
try:
return base64.b64decode(signature.strip(), validate=True)
except ValueError:
return signature
def build_csr(args: argparse.Namespace, spki_der: bytes) -> bytes:
cri = der_sequence(
der_integer(0),
subject_name(args.common_name, args.email_address, args.country),
spki_der,
b"\xA0\x00",
)
signature = kms_sign(args, cri)
signature_algorithm = der_sequence(
der_oid("1.2.840.113549.1.1.11"),
der_null(),
)
return der_sequence(cri, signature_algorithm, der_bit_string(signature))
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate an Apple-compatible CSR backed by a Google Cloud KMS RSA signing key.",
)
parser.add_argument("--gcloud-project", default=os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get("BURROW_GOOGLE_PROJECT_ID") or DEFAULT_PROJECT)
parser.add_argument("--kms-location", default=os.environ.get("BURROW_KMS_LOCATION", DEFAULT_LOCATION))
parser.add_argument("--kms-keyring", default=os.environ.get("BURROW_KMS_KEYRING", DEFAULT_KEYRING))
parser.add_argument("--kms-key", default=os.environ.get("BURROW_APPLE_KMS_KEY") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_KMS_KEY", DEFAULT_KEY))
parser.add_argument("--kms-version", default=os.environ.get("BURROW_KMS_VERSION", DEFAULT_VERSION))
parser.add_argument("--common-name", default=os.environ.get("BURROW_APPLE_CSR_COMMON_NAME") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_CSR_COMMON_NAME", DEFAULT_COMMON_NAME))
parser.add_argument("--email-address", default=os.environ.get("BURROW_APPLE_CSR_EMAIL") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_CSR_EMAIL", DEFAULT_EMAIL))
parser.add_argument("--country", default=os.environ.get("BURROW_APPLE_CSR_COUNTRY") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_CSR_COUNTRY", "US"))
parser.add_argument("--public-key-pem", help="Existing KMS public-key PEM path. If omitted, gcloud fetches it.")
parser.add_argument("--public-key-output", help="Write the fetched/used public key PEM to this path.")
parser.add_argument("--output", default="developer-id-application.certSigningRequest")
parser.add_argument("--gcloud", default=os.environ.get("GCLOUD", "gcloud"))
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
output = Path(args.output)
with tempfile.TemporaryDirectory(prefix="burrow-kms-csr.") as tmp:
public_key_path = Path(args.public_key_pem) if args.public_key_pem else Path(tmp) / "kms-public.pem"
if not args.public_key_pem:
fetch_public_key(args, public_key_path)
public_key_pem = public_key_path.read_bytes()
spki_der = pem_to_der(public_key_pem)
csr = build_csr(args, spki_der)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(pem_wrap("CERTIFICATE REQUEST", csr), encoding="ascii")
if args.public_key_output:
public_output = Path(args.public_key_output)
public_output.parent.mkdir(parents=True, exist_ok=True)
public_output.write_bytes(public_key_pem)
print(f"wrote {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

View file

@ -0,0 +1,327 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { webcrypto } from "node:crypto";
const crypto = globalThis.crypto ?? webcrypto;
function b64url(input) {
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
return buf
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function parseArgs(argv) {
const out = { _: [] };
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") out.help = true;
else if (arg.startsWith("--")) {
const key = arg.slice(2);
const value = argv[i + 1];
if (!value || value.startsWith("--")) throw new Error(`missing value for --${key}`);
out[key] = value;
i++;
} else {
out._.push(arg);
}
}
return out;
}
async function makeJwt({ keyId, issuerId, keyPem }) {
const header = { alg: "ES256", kid: keyId, typ: "JWT" };
const now = Math.floor(Date.now() / 1000);
const payload = { iss: issuerId, exp: now + 20 * 60, aud: "appstoreconnect-v1" };
const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
const pkcs8Der = Buffer.from(
keyPem.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\s+/g, ""),
"base64",
);
const key = await crypto.subtle.importKey(
"pkcs8",
pkcs8Der,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
key,
new TextEncoder().encode(input),
);
return `${input}.${b64url(Buffer.from(sig))}`;
}
async function ascFetch(jwt, url, opts = {}) {
const res = await fetch(url, {
...opts,
headers: {
...(opts.headers ?? {}),
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
},
});
const text = await res.text();
let body;
try {
body = text ? JSON.parse(text) : {};
} catch {
body = { raw: text };
}
if (!res.ok) {
throw new Error(`ASC ${res.status} ${url}: ${JSON.stringify(body).slice(0, 2000)}`);
}
return body;
}
function truthy(value) {
return ["1", "true", "yes", "on"].includes(String(value ?? "").toLowerCase());
}
function safeFileName(identifier, suffix) {
return `${identifier.replace(/[^A-Za-z0-9]/g, "").toLowerCase()}${suffix}`;
}
function bundleDisplayName(identifier, appId) {
return identifier === appId ? "Burrow App" : "Burrow Network Extension";
}
async function findCertificate(jwt, preferredTypes, preferredId) {
const certs = await ascFetch(
jwt,
"https://api.appstoreconnect.apple.com/v1/certificates?limit=200",
);
const data = certs.data ?? [];
if (preferredId) {
const cert = data.find((item) => item?.id === preferredId);
if (!cert) throw new Error(`unable to locate certificate id ${preferredId} via ASC API`);
const type = cert?.attributes?.certificateType;
if (preferredTypes.length && !preferredTypes.includes(type)) {
throw new Error(`certificate ${preferredId} has type ${type}, expected ${preferredTypes.join(" or ")}`);
}
return cert;
}
for (const type of preferredTypes) {
const cert = data.find((item) => item?.attributes?.certificateType === type);
if (cert) return cert;
}
throw new Error(`unable to locate certificate type ${preferredTypes.join(" or ")} via ASC API`);
}
async function resolveBundle(jwt, target, { createMissing, platform }) {
const bundleResp = await ascFetch(
jwt,
`https://api.appstoreconnect.apple.com/v1/bundleIds?limit=5&filter[identifier]=${encodeURIComponent(target.identifier)}`,
);
let bundle = (bundleResp.data ?? []).find(
(item) => item?.attributes?.identifier === target.identifier,
);
if (bundle || !createMissing) return bundle;
const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/bundleIds", {
method: "POST",
body: JSON.stringify({
data: {
type: "bundleIds",
attributes: {
identifier: target.identifier,
name: target.bundleName ?? target.name,
platform,
},
},
}),
});
bundle = created?.data;
if (bundle?.id) {
process.stdout.write(`created bundle id ${target.identifier} (${platform})\n`);
}
return bundle;
}
async function listCapabilityTypes(jwt, bundleId) {
const resp = await ascFetch(
jwt,
`https://api.appstoreconnect.apple.com/v1/bundleIds/${bundleId}/bundleIdCapabilities`,
);
return new Set((resp.data ?? []).map((item) => item?.attributes?.capabilityType).filter(Boolean));
}
async function enableCapability(jwt, bundleId, capabilityType, settings) {
await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities", {
method: "POST",
body: JSON.stringify({
data: {
type: "bundleIdCapabilities",
attributes: {
capabilityType,
...(settings ? { settings } : {}),
},
relationships: {
bundleId: {
data: {
id: bundleId,
type: "bundleIds",
},
},
},
},
}),
});
}
async function profileIncludesCertificate(jwt, profileId, certificateId) {
const resp = await ascFetch(
jwt,
`https://api.appstoreconnect.apple.com/v1/profiles/${profileId}/certificates?limit=200`,
);
return (resp.data ?? []).some((item) => item?.id === certificateId);
}
async function ensureCapabilities(jwt, bundle, capabilities) {
const existing = await listCapabilityTypes(jwt, bundle.id);
for (const capability of capabilities) {
if (existing.has(capability.type)) continue;
try {
await enableCapability(jwt, bundle.id, capability.type, capability.settings);
process.stdout.write(`enabled ${capability.type} for ${bundle.attributes.identifier}\n`);
} catch (err) {
process.stderr.write(`warning: could not enable ${capability.type} for ${bundle.attributes.identifier}: ${err.message}\n`);
}
}
}
async function syncTarget(jwt, cert, profileType, target, outDir, options) {
const bundle = await resolveBundle(jwt, target, options);
if (!bundle) throw new Error(`bundleId not found in ASC: ${target.identifier}`);
if (options.enableCapabilities && target.capabilities?.length) {
await ensureCapabilities(jwt, bundle, target.capabilities);
}
const existing = await ascFetch(
jwt,
`https://api.appstoreconnect.apple.com/v1/profiles?limit=10&filter[profileType]=${encodeURIComponent(profileType)}&filter[name]=${encodeURIComponent(target.name)}`,
);
let profileId = (existing.data ?? []).find((item) => item?.attributes?.name === target.name)?.id;
if (profileId && options.replaceCertificateMismatch) {
const includesCertificate = await profileIncludesCertificate(jwt, profileId, cert.id);
if (!includesCertificate) {
await ascFetch(jwt, `https://api.appstoreconnect.apple.com/v1/profiles/${profileId}`, {
method: "DELETE",
});
process.stdout.write(`deleted stale profile ${target.name}; it did not include certificate ${cert.id}\n`);
profileId = undefined;
}
}
if (!profileId) {
const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/profiles", {
method: "POST",
body: JSON.stringify({
data: {
type: "profiles",
attributes: { name: target.name, profileType },
relationships: {
bundleId: { data: { type: "bundleIds", id: bundle.id } },
certificates: { data: [{ type: "certificates", id: cert.id }] },
},
},
}),
});
profileId = created?.data?.id;
}
if (!profileId) throw new Error(`failed to resolve/create profile for ${target.identifier}`);
const profile = await ascFetch(jwt, `https://api.appstoreconnect.apple.com/v1/profiles/${profileId}`);
const content = profile?.data?.attributes?.profileContent;
if (!content) throw new Error(`profileContent missing for ${target.name} (${profileId})`);
fs.mkdirSync(outDir, { recursive: true });
const profileBytes = Buffer.from(content, "base64");
const profilePath = path.join(outDir, target.file);
fs.writeFileSync(profilePath, profileBytes);
if (target.mobileFile) {
fs.writeFileSync(path.join(outDir, target.mobileFile), profileBytes);
}
process.stdout.write(`synced ${target.identifier} -> ${profilePath}\n`);
}
async function main() {
const args = parseArgs(process.argv);
if (args.help) {
console.log("Usage: sync-provisioning-profiles.mjs --platform ios|macos|all --key-id ID --issuer-id ID --key-file AuthKey.p8 --out-dir DIR [--ios-certificate-id ID] [--macos-certificate-id ID] [--replace-certificate-mismatch true]");
process.exit(0);
}
const platform = args.platform ?? "all";
const keyId = args["key-id"];
const issuerId = args["issuer-id"];
const keyFile = args["key-file"];
const outDir = args["out-dir"] ?? ".forgejo/actions/export";
const appId = args["app-id"] ?? "com.hackclub.burrow";
const networkId = args["network-id"] ?? `${appId}.network`;
const iosCertificateId = args["ios-certificate-id"] ?? process.env.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID;
const macosCertificateId = args["macos-certificate-id"] ?? process.env.BURROW_DEVELOPER_ID_CERTIFICATE_ID;
const createMissing = truthy(args["create-missing-bundle-ids"]);
const enableCapabilities = createMissing || truthy(args["enable-capabilities"]);
const replaceCertificateMismatch = truthy(args["replace-certificate-mismatch"]);
const bundlePlatform = args["bundle-platform"] ?? "UNIVERSAL";
const associatedDomains = (args["associated-domains"] ?? "applinks:burrow.rs?mode=developer,webcredentials:burrow.rs?mode=developer")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
const appCapabilities = [
{ type: "NETWORK_EXTENSIONS" },
{ type: "APP_GROUPS" },
{
type: "ASSOCIATED_DOMAINS",
settings: [{ key: "ASSOCIATED_DOMAIN_IDS", options: associatedDomains.map((key) => ({ key, enabled: true })) }],
},
];
const extensionCapabilities = [
{ type: "NETWORK_EXTENSIONS" },
{ type: "APP_GROUPS" },
];
const options = { createMissing, enableCapabilities, replaceCertificateMismatch, platform: bundlePlatform };
if (!keyId || !issuerId || !keyFile) throw new Error("required: --key-id --issuer-id --key-file");
const jwt = await makeJwt({ keyId, issuerId, keyPem: fs.readFileSync(keyFile, "utf8") });
if (platform === "ios" || platform === "all") {
const cert = await findCertificate(jwt, ["IOS_DISTRIBUTION", "DISTRIBUTION", "APPLE_DISTRIBUTION"], iosCertificateId);
const targets = [appId, networkId].map((identifier) => ({
identifier,
name: `${identifier}.app-store`,
bundleName: bundleDisplayName(identifier, appId),
file: safeFileName(identifier, "appstore.provisionprofile"),
mobileFile: safeFileName(identifier, "appstore.mobileprovision"),
capabilities: identifier === appId ? appCapabilities : extensionCapabilities,
}));
for (const target of targets) {
await syncTarget(jwt, cert, "IOS_APP_STORE", target, outDir, options);
}
}
if (platform === "macos" || platform === "all") {
const cert = await findCertificate(jwt, ["DEVELOPER_ID_APPLICATION"], macosCertificateId);
const targets = [appId, networkId].map((identifier) => ({
identifier,
name: `${identifier}.developer-id`,
bundleName: bundleDisplayName(identifier, appId),
file: safeFileName(identifier, "developerid.provisionprofile"),
capabilities: identifier === appId ? appCapabilities : extensionCapabilities,
}));
for (const target of targets) {
await syncTarget(jwt, cert, "MAC_APP_DIRECT", target, outDir, options);
}
}
}
main().catch((err) => {
console.error(err?.stack || String(err));
process.exit(1);
});