From 87d4e41b3219e8166ab162f2e920aab97a9d373c Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Fri, 18 Sep 2026 08:54:56 -0700 Subject: [PATCH] embed apple's root certificates so the iap endpoint can verify on lith *.cer is gitignored, so the deploy pulled a tree without backend/apple-roots and every /api/easel-iap call died in readdirSync. The two DER roots now live base64-encoded in backend/apple-roots.mjs. Co-Authored-By: Claude Fable 5.1 --- system/backend/apple-roots.mjs | 9 +++++++++ system/netlify/functions/easel-iap.mjs | 10 ++-------- 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 system/backend/apple-roots.mjs diff --git a/system/backend/apple-roots.mjs b/system/backend/apple-roots.mjs new file mode 100644 index 0000000000..c2aabfd7f7 --- /dev/null +++ b/system/backend/apple-roots.mjs @@ -0,0 +1,9 @@ +// Apple root certificates (DER, from https://www.apple.com/certificateauthority/), +// embedded so the deploy carries them (*.cer is gitignored). App Store signed +// data chains to Apple Root CA - G3; G2 is kept for completeness. +import { Buffer } from 'node:buffer'; +export const APPLE_ROOTS = Object.freeze({ + 'AppleRootCA-G2': 'MIIFkjCCA3qgAwIBAgIIAeDltYNno+AwDQYJKoZIhvcNAQEMBQAwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEcyMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxMDA5WhcNMzkwNDMwMTgxMDA5WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzIxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgREkhI2imKScUcx+xuM23+TfvgHN6sXuI2pyT5f1BrTM65MFQn5bPW7SXmMLYFN14UIhHF6Kob0vuy0gmVOKTvKkmMXT5xZgM4+xb1hYjkWpIMBDLyyED7Ul+f9sDx47pFoFDVEovy3d6RhiPw9bZyLgHaC/YuOQhfGaFjQQscp5TBhsRTL3b2CtcM0YM/GlMZ81fVJ3/8E7j4ko380yhDPLVoACVdJ2LT3VXdRCCQgzWTxb+4Gftr49wIQuavbfqeQMpOhYV4SbHXw8EwOTKrfl+q04tvny0aIWhwZ7Oj8ZhBbZF8+NfbqOdfIRqMM78xdLe40fTgIvS/cjTf94FNcX1RoeKz8NMoFnNvzcytN31O661A4T+B/fc9Cj6i8b0xlilZ3MIZgIxbdMYs0xBTJh0UT8TUgWY8h2czJxQI6bR3hDRSj4n4aJgXv8O7qhOTH11UL6jHfPsNFL4VPSQ08prcdUFmIrQB1guvkJ4M6mL4m1k8COKWNORj3rw31OsMiANDC1CvoDTdUE0V+1ok2Az6DGOeHwOx4e7hqkP0ZmUoNwIx7wHHHtHMn23KVDpA287PT0aLSmWaasZobNfMmRtHsHLDd4/E92GcdB/O/WuhwpyUgquUoue9G7q5cDmVF8Up8zlYNPXEpMZ7YLlmQ1A/bmH8DvmGqmAMQ0uVAgMBAAGjQjBAMB0GA1UdDgQWBBTEmRNsGAPCe8CjoA1/coB6HHcmjTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAUabz4vS4PZO/Lc4Pu1vhVRROTtHlznldgX/+tvCHM/jvlOV+3Gp5pxy+8JS3ptEwnMgNCnWefZKVfhidfsJxaXwU6s+DDuQUQp50DhDNqxq6EWGBeNjxtUVAeKuowM77fWM3aPbn+6/Gw0vsHzYmE1SGlHKy6gLti23kDKaQwFd1z4xCfVzmMX3zybKSaUYOiPjjLUKyOKimGY3xn83uamW8GrAlvacp/fQ+onVJv57byfenHmOZ4VxG/5IFjPoeIPmGlFYl5bRXOJ3riGQUIUkhOb9iZqmxospvPyFgxYnURTbImHy99v6ZSYA7LNKmp4gDBDEZt7Y6YUX6yfIjyGNzv1aJMbDZfGKnexWoiIqrOEDCzBL/FePwN983csvMmOa/orz6JopxVtfnJBtIRD6e/J/JzBrsQzwBvDR4yGn1xuZW7AYJNpDrFEobXsmII9oDMJELuDY++ee1KG++P+w8j2Ud5cAeh6Squpj9kuNsJnfdBrRkBof0Tta6SqoWqPQFZ2aWuuJVecMsXUmPgEkrihLHdoBR37q9ZV0+N0djMenl9MU/S60EinpxLK8JQzcPqOMyT/RFtm2XNuyE9QoB6he7hY1Ck3DDUOUUi78/w0EP3SIEIwiKum1xRKtzCTrJ+VKACd+66eYWyi4uTLLT3OUEVLLUNIAytbwPF+E=', + 'AppleRootCA-G3': 'MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtfTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySrMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM6BgD56KyKA==', +}); +export function appleRootCertificates() { return Object.values(APPLE_ROOTS).map(value => Buffer.from(value, 'base64')); } diff --git a/system/netlify/functions/easel-iap.mjs b/system/netlify/functions/easel-iap.mjs index e8e96cf6b6..07457adc07 100644 --- a/system/netlify/functions/easel-iap.mjs +++ b/system/netlify/functions/easel-iap.mjs @@ -6,11 +6,9 @@ // the Apple transaction id, so a retried redemption or a duplicate // notification cannot credit twice. App Store Server Notifications (REFUND) // arrive at the same address with a `signedPayload` and take the pack back. -import { readFileSync, readdirSync } from 'node:fs'; -import { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { SignedDataVerifier, Environment } from '@apple/app-store-server-library'; import { authorize } from '../../backend/authorization.mjs'; +import { appleRootCertificates } from '../../backend/apple-roots.mjs'; import { IAP_PRODUCTS, withWallets, fulfillGrant, revokeGrant } from '../../backend/easel-paid-credits.mjs'; export const BUNDLE_ID='computer.aesthetic.easel'; @@ -18,11 +16,7 @@ export const APP_APPLE_ID=6812823093; const headers={'Content-Type':'application/json','Cache-Control':'no-store','Access-Control-Allow-Origin':'*','Access-Control-Allow-Headers':'Authorization, Content-Type','Access-Control-Allow-Methods':'POST, OPTIONS'}; const reply=(statusCode,value)=>({statusCode,headers,body:JSON.stringify(value)}); -function appleRoots(){ - const dir=join(dirname(fileURLToPath(import.meta.url)),'../../backend/apple-roots'); - return readdirSync(dir).filter(name=>name.endsWith('.cer')).map(name=>readFileSync(join(dir,name))); -} -export function makeVerifiers({roots=appleRoots(),sandbox=process.env.AC_IAP_ALLOW_SANDBOX==='true',online=true}={}){ +export function makeVerifiers({roots=appleRootCertificates(),sandbox=process.env.AC_IAP_ALLOW_SANDBOX==='true',online=true}={}){ const list=[new SignedDataVerifier(roots,online,Environment.PRODUCTION,BUNDLE_ID,APP_APPLE_ID)]; if(sandbox)list.push(new SignedDataVerifier(roots,online,Environment.SANDBOX,BUNDLE_ID,APP_APPLE_ID)); return list; -- 2.51.2