@palbase/backend 25.0.3 → 25.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/palbase-backend.cjs +279 -15
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +3 -3
- package/dist/{chunk-P2Q27SGP.js → chunk-CJSKYY76.js} +182 -2
- package/dist/chunk-CJSKYY76.js.map +1 -0
- package/dist/{chunk-XJ2RSHEU.js → chunk-CRQKCRGF.js} +16 -2
- package/dist/chunk-CRQKCRGF.js.map +1 -0
- package/dist/{chunk-7JSEN7UR.js → chunk-G4R6BTLV.js} +3 -182
- package/dist/chunk-G4R6BTLV.js.map +1 -0
- package/dist/{chunk-WH2EV2LT.js → chunk-VDF2T4AS.js} +265 -17
- package/dist/chunk-VDF2T4AS.js.map +1 -0
- package/dist/db/index.cjs +1 -1
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.js +2 -2
- package/dist/engine/index.cjs +279 -15
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.js +3 -3
- package/dist/index.cjs +15 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -6
- package/docs/README.md +4 -3
- package/docs/llms-full.txt +4 -3
- package/package.json +1 -1
- package/template/AGENTS.md +3 -1
- package/template/controllers/notes.controller.ts +2 -2
- package/template/services/note.service.ts +5 -3
- package/dist/chunk-7JSEN7UR.js.map +0 -1
- package/dist/chunk-P2Q27SGP.js.map +0 -1
- package/dist/chunk-WH2EV2LT.js.map +0 -1
- package/dist/chunk-XJ2RSHEU.js.map +0 -1
|
@@ -492,6 +492,20 @@ function makeTableProxy(ops, prefix) {
|
|
|
492
492
|
findById: (id) => ops().findById(name, id),
|
|
493
493
|
findMany: (query, opts) => ops().findMany(name, query, opts),
|
|
494
494
|
upsert: (data, opts) => ops().upsert(name, data, opts),
|
|
495
|
+
// THREE VERBS THE TYPE PROMISED AND THIS PROXY DID NOT EMIT.
|
|
496
|
+
//
|
|
497
|
+
// `EnvTypedTableBase` declares `updateMany`, `deleteMany` and `count`
|
|
498
|
+
// (typed-db.ts) and the ops layer implements all three — only this
|
|
499
|
+
// proxy, which is what a handler actually touches, left them out. So
|
|
500
|
+
// the type said the verb exists, autocomplete offered it, and the call
|
|
501
|
+
// answered `undefined is not a function`.
|
|
502
|
+
//
|
|
503
|
+
// Older than this run, but the run rewrote this proxy for
|
|
504
|
+
// `Database.schema(name).tables.*` and would have carried the gap onto
|
|
505
|
+
// the new surface too.
|
|
506
|
+
updateMany: (where, set) => ops().updateMany(name, where, set),
|
|
507
|
+
deleteMany: (where) => ops().deleteMany(name, where),
|
|
508
|
+
count: (where) => ops().count(name, where),
|
|
495
509
|
search: (params) => ops().search(name, params),
|
|
496
510
|
similar: (id, params) => ops().similar(name, id, params),
|
|
497
511
|
recommend: (params) => ops().recommend(name, params),
|
|
@@ -1093,6 +1107,11 @@ function assertUsableWriteValues(caller, table, cols, data) {
|
|
|
1093
1107
|
}
|
|
1094
1108
|
}
|
|
1095
1109
|
|
|
1110
|
+
// src/db/schema-json.ts
|
|
1111
|
+
function qualifiedTableKey(schemaName, tableName) {
|
|
1112
|
+
return schemaName === "" || schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1096
1115
|
// src/engine/db.ts
|
|
1097
1116
|
function quoteIdent(name) {
|
|
1098
1117
|
return `"${name.replace(/"/g, '""')}"`;
|
|
@@ -2255,9 +2274,6 @@ function setSchema(schemas) {
|
|
|
2255
2274
|
}
|
|
2256
2275
|
currentSchema = { tables };
|
|
2257
2276
|
}
|
|
2258
|
-
function qualifiedTableKey(schema, table) {
|
|
2259
|
-
return schema === "" || schema === "public" ? table : `${schema}.${table}`;
|
|
2260
|
-
}
|
|
2261
2277
|
function withTables(ops, schema = currentSchema) {
|
|
2262
2278
|
const bind = (key) => ({
|
|
2263
2279
|
insert: (data) => ops.insert(key, data),
|
|
@@ -2651,6 +2667,182 @@ function matchRoute(table, method, pathname) {
|
|
|
2651
2667
|
return null;
|
|
2652
2668
|
}
|
|
2653
2669
|
|
|
2670
|
+
// src/engine/attest.ts
|
|
2671
|
+
var HEADER_DEVICE_ID = "x-palbase-device-id";
|
|
2672
|
+
var HEADER_PAYLOAD = "x-palbase-attest-payload";
|
|
2673
|
+
var HEADER_SIGNATURE = "x-palbase-attest-signature";
|
|
2674
|
+
var HEADER_CHALLENGE = "x-palbase-attest-challenge";
|
|
2675
|
+
var HEADER_CAPABLE = "x-palbase-attest-capable";
|
|
2676
|
+
var HEADER_INSTALLATION = "x-palbase-installation";
|
|
2677
|
+
var HEADER_ANCHOR = "x-palbase-installation-anchor";
|
|
2678
|
+
var DEFAULT_POLICY_TTL_MS = 3e4;
|
|
2679
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 2e3;
|
|
2680
|
+
async function bodyDigest(body) {
|
|
2681
|
+
if (!body || body.byteLength === 0) return "";
|
|
2682
|
+
const bytes = new Uint8Array(new ArrayBuffer(body.byteLength));
|
|
2683
|
+
bytes.set(body);
|
|
2684
|
+
const buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
2685
|
+
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
2686
|
+
}
|
|
2687
|
+
function readCapable(req) {
|
|
2688
|
+
const v = req.headers.get(HEADER_CAPABLE);
|
|
2689
|
+
return v === "yes" || v === "no" ? v : "unknown";
|
|
2690
|
+
}
|
|
2691
|
+
function createAttestGate(deps) {
|
|
2692
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
2693
|
+
const ttl = deps.policyTtlMs ?? DEFAULT_POLICY_TTL_MS;
|
|
2694
|
+
const policyTimeoutMs = deps.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2695
|
+
let cachedMode = "off";
|
|
2696
|
+
let cachedAt = -Infinity;
|
|
2697
|
+
async function mode() {
|
|
2698
|
+
if (Date.now() - cachedAt < ttl) return cachedMode;
|
|
2699
|
+
try {
|
|
2700
|
+
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/attest-policy`, {
|
|
2701
|
+
headers: { apikey: deps.serviceRoleKey },
|
|
2702
|
+
signal: AbortSignal.timeout(policyTimeoutMs)
|
|
2703
|
+
});
|
|
2704
|
+
if (!res.ok) throw new Error(`attest-policy ${res.status}`);
|
|
2705
|
+
const body = await res.json();
|
|
2706
|
+
cachedMode = body.mode === "monitor" || body.mode === "enforce" ? body.mode : "off";
|
|
2707
|
+
cachedAt = Date.now();
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
return cachedMode;
|
|
2711
|
+
}
|
|
2712
|
+
return async function gate(req, body, _ctx) {
|
|
2713
|
+
const m = await mode();
|
|
2714
|
+
if (m === "off") {
|
|
2715
|
+
return { outcome: "pass", reason: "", device: null, observation: null, nextChallenge: null };
|
|
2716
|
+
}
|
|
2717
|
+
const deviceId = req.headers.get(HEADER_DEVICE_ID) ?? "";
|
|
2718
|
+
const payload = req.headers.get(HEADER_PAYLOAD) ?? "";
|
|
2719
|
+
const signature = req.headers.get(HEADER_SIGNATURE) ?? "";
|
|
2720
|
+
const hasEvidence = deviceId !== "" && payload !== "" && signature !== "";
|
|
2721
|
+
const observation = {
|
|
2722
|
+
capable: readCapable(req),
|
|
2723
|
+
attested: false,
|
|
2724
|
+
installationId: req.headers.get(HEADER_INSTALLATION) || null
|
|
2725
|
+
};
|
|
2726
|
+
if (m === "monitor") {
|
|
2727
|
+
observation.attested = hasEvidence;
|
|
2728
|
+
return { outcome: "pass", reason: "", device: null, observation, nextChallenge: null };
|
|
2729
|
+
}
|
|
2730
|
+
const url = new URL(req.url);
|
|
2731
|
+
let verdict;
|
|
2732
|
+
try {
|
|
2733
|
+
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/assert`, {
|
|
2734
|
+
method: "POST",
|
|
2735
|
+
headers: { apikey: deps.serviceRoleKey, "content-type": "application/json" },
|
|
2736
|
+
// A HUNG palauth must not become a hung tenant. Without this, a
|
|
2737
|
+
// process that accepts the connection and never answers holds every
|
|
2738
|
+
// request open until the client gives up — the gate would take the
|
|
2739
|
+
// whole stack down while claiming to protect it.
|
|
2740
|
+
signal: AbortSignal.timeout(policyTimeoutMs),
|
|
2741
|
+
body: JSON.stringify({
|
|
2742
|
+
installation_id: req.headers.get(HEADER_ANCHOR) ?? "",
|
|
2743
|
+
device_id: deviceId,
|
|
2744
|
+
challenge: req.headers.get(HEADER_CHALLENGE) ?? "",
|
|
2745
|
+
payload,
|
|
2746
|
+
signature,
|
|
2747
|
+
method: req.method,
|
|
2748
|
+
// pathname + search, NOT pathname alone.
|
|
2749
|
+
//
|
|
2750
|
+
// The client signs the path string it was given, and that string can
|
|
2751
|
+
// carry a query — the transport splits and reattaches one on purpose.
|
|
2752
|
+
// Recomputing from pathname alone would drop the query from the
|
|
2753
|
+
// signed definition, which breaks it BOTH ways: an attested call
|
|
2754
|
+
// carrying a query would fail to verify, and a captured assertion
|
|
2755
|
+
// could be replayed against the same route with different query
|
|
2756
|
+
// parameters, which is exactly the substitution the binding exists to
|
|
2757
|
+
// prevent.
|
|
2758
|
+
path: url.pathname + url.search,
|
|
2759
|
+
body_sha256: await bodyDigest(body)
|
|
2760
|
+
})
|
|
2761
|
+
});
|
|
2762
|
+
if (!res.ok) throw new Error(`assert ${res.status}`);
|
|
2763
|
+
verdict = await res.json();
|
|
2764
|
+
} catch {
|
|
2765
|
+
return {
|
|
2766
|
+
outcome: "reject",
|
|
2767
|
+
reason: "app_attest_required",
|
|
2768
|
+
device: null,
|
|
2769
|
+
observation,
|
|
2770
|
+
nextChallenge: null
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
if (!verdict.ok) {
|
|
2774
|
+
const reason2 = verdict.reason === "assertion_invalid" || verdict.reason === "assertion_stale" ? verdict.reason : "app_attest_required";
|
|
2775
|
+
return {
|
|
2776
|
+
outcome: "reject",
|
|
2777
|
+
reason: reason2,
|
|
2778
|
+
device: null,
|
|
2779
|
+
observation,
|
|
2780
|
+
nextChallenge: verdict.next_challenge || null
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
observation.attested = true;
|
|
2784
|
+
return {
|
|
2785
|
+
outcome: "pass",
|
|
2786
|
+
reason: "",
|
|
2787
|
+
device: {
|
|
2788
|
+
installationId: "",
|
|
2789
|
+
authDeviceId: deviceId,
|
|
2790
|
+
platform: "ios",
|
|
2791
|
+
assurance: "attested"
|
|
2792
|
+
},
|
|
2793
|
+
observation,
|
|
2794
|
+
nextChallenge: verdict.next_challenge || null
|
|
2795
|
+
};
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
function createObservationSink(deps) {
|
|
2799
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
2800
|
+
const maxBatch = deps.maxBatch ?? 1e3;
|
|
2801
|
+
const intervalMs = deps.intervalMs ?? 1e4;
|
|
2802
|
+
let buffer = [];
|
|
2803
|
+
let inFlight = Promise.resolve();
|
|
2804
|
+
const timer = setInterval(() => {
|
|
2805
|
+
void flush();
|
|
2806
|
+
}, intervalMs);
|
|
2807
|
+
timer.unref?.();
|
|
2808
|
+
function bucketsOf(batch) {
|
|
2809
|
+
const counts = { capable: 0, incapable: 0, unknown: 0, no_sdk: 0 };
|
|
2810
|
+
for (const o of batch) {
|
|
2811
|
+
if (o.attested || o.capable === "yes") counts.capable += 1;
|
|
2812
|
+
else if (o.capable === "no") counts.incapable += 1;
|
|
2813
|
+
else if (o.installationId) counts.unknown += 1;
|
|
2814
|
+
else counts.no_sdk += 1;
|
|
2815
|
+
}
|
|
2816
|
+
return counts;
|
|
2817
|
+
}
|
|
2818
|
+
async function flush() {
|
|
2819
|
+
if (buffer.length === 0) return;
|
|
2820
|
+
const batch = buffer;
|
|
2821
|
+
buffer = [];
|
|
2822
|
+
inFlight = (async () => {
|
|
2823
|
+
try {
|
|
2824
|
+
await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/observations`, {
|
|
2825
|
+
method: "POST",
|
|
2826
|
+
headers: { apikey: deps.serviceRoleKey, "content-type": "application/json" },
|
|
2827
|
+
body: JSON.stringify(bucketsOf(batch))
|
|
2828
|
+
});
|
|
2829
|
+
} catch {
|
|
2830
|
+
}
|
|
2831
|
+
})();
|
|
2832
|
+
await inFlight;
|
|
2833
|
+
}
|
|
2834
|
+
return {
|
|
2835
|
+
record(o) {
|
|
2836
|
+
buffer.push(o);
|
|
2837
|
+
if (buffer.length >= maxBatch) void flush();
|
|
2838
|
+
},
|
|
2839
|
+
flush,
|
|
2840
|
+
stop() {
|
|
2841
|
+
clearInterval(timer);
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2654
2846
|
// src/engine/upload.ts
|
|
2655
2847
|
var AUTHORIZE_PATH = "/__palbase/upload/authorize";
|
|
2656
2848
|
var SIGNATURE_HEADER = "x-palbase-upload-signature";
|
|
@@ -2743,6 +2935,7 @@ function makeSseWriter(opts) {
|
|
|
2743
2935
|
}
|
|
2744
2936
|
|
|
2745
2937
|
// src/engine/index.ts
|
|
2938
|
+
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
2746
2939
|
var JSON_HEADERS = { "content-type": "application/json" };
|
|
2747
2940
|
function fieldErrors(err) {
|
|
2748
2941
|
return err.issues.map((i) => ({ field: i.path.join("."), message: i.message }));
|
|
@@ -2803,6 +2996,14 @@ async function createApp(opts) {
|
|
|
2803
2996
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
2804
2997
|
await sql.unsafe("select 1");
|
|
2805
2998
|
const auth = new AuthVerifier({ jwksUrl: config.authJwksUrl, issuer: config.authIssuer });
|
|
2999
|
+
const attestGate = createAttestGate({
|
|
3000
|
+
moduleBaseUrl: config.moduleBaseUrl,
|
|
3001
|
+
serviceRoleKey: config.serviceRoleKey
|
|
3002
|
+
});
|
|
3003
|
+
const observations = createObservationSink({
|
|
3004
|
+
moduleBaseUrl: config.moduleBaseUrl,
|
|
3005
|
+
serviceRoleKey: config.serviceRoleKey
|
|
3006
|
+
});
|
|
2806
3007
|
const limiter = new RateLimiter();
|
|
2807
3008
|
const cache = opts.cache ?? makeMemoryCache();
|
|
2808
3009
|
const log = opts.logger ?? console;
|
|
@@ -2884,6 +3085,8 @@ async function createApp(opts) {
|
|
|
2884
3085
|
const hit = matchRoute(routes, req.method, url.pathname);
|
|
2885
3086
|
if (!hit) return envelope("not_found", "No route matches this method and path", 404, requestId);
|
|
2886
3087
|
const { meta } = hit.entry;
|
|
3088
|
+
let attestedDevice = null;
|
|
3089
|
+
let attestChallengeOut = null;
|
|
2887
3090
|
const spec = effectiveAuth(meta.options?.auth, hit.entry.controllerAuth);
|
|
2888
3091
|
const claims = await auth.verify(req.headers.get("authorization"));
|
|
2889
3092
|
if (spec.required && !claims) {
|
|
@@ -2896,6 +3099,67 @@ async function createApp(opts) {
|
|
|
2896
3099
|
if (claims && spec.verifiedEmail && claims.email_verified !== true) {
|
|
2897
3100
|
return envelope("email_not_verified", "A verified email address is required", 403, requestId);
|
|
2898
3101
|
}
|
|
3102
|
+
let parsedBody;
|
|
3103
|
+
let bodyRead = false;
|
|
3104
|
+
let rawBody = null;
|
|
3105
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
3106
|
+
const declared = Number(req.headers.get("content-length") ?? "");
|
|
3107
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
3108
|
+
return new Response(
|
|
3109
|
+
JSON.stringify({
|
|
3110
|
+
error: "payload_too_large",
|
|
3111
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
3112
|
+
status: 413,
|
|
3113
|
+
request_id: requestId
|
|
3114
|
+
}),
|
|
3115
|
+
{ status: 413, headers: JSON_HEADERS }
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
const buf = await req.arrayBuffer().catch(() => null);
|
|
3119
|
+
if (buf && buf.byteLength > MAX_BODY_BYTES) {
|
|
3120
|
+
return new Response(
|
|
3121
|
+
JSON.stringify({
|
|
3122
|
+
error: "payload_too_large",
|
|
3123
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
3124
|
+
status: 413,
|
|
3125
|
+
request_id: requestId
|
|
3126
|
+
}),
|
|
3127
|
+
{ status: 413, headers: JSON_HEADERS }
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
|
|
3131
|
+
}
|
|
3132
|
+
const readParsedBody = () => {
|
|
3133
|
+
if (bodyRead) return parsedBody;
|
|
3134
|
+
bodyRead = true;
|
|
3135
|
+
if (!rawBody) {
|
|
3136
|
+
parsedBody = {};
|
|
3137
|
+
return parsedBody;
|
|
3138
|
+
}
|
|
3139
|
+
try {
|
|
3140
|
+
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
3141
|
+
} catch {
|
|
3142
|
+
parsedBody = {};
|
|
3143
|
+
}
|
|
3144
|
+
return parsedBody;
|
|
3145
|
+
};
|
|
3146
|
+
const attest = await attestGate(req, rawBody, { userId });
|
|
3147
|
+
if (attest.observation) observations.record(attest.observation);
|
|
3148
|
+
if (attest.outcome === "reject") {
|
|
3149
|
+
const headers = { ...JSON_HEADERS };
|
|
3150
|
+
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
3151
|
+
return new Response(
|
|
3152
|
+
JSON.stringify({
|
|
3153
|
+
error: attest.reason,
|
|
3154
|
+
error_description: "This endpoint requires a verified app instance",
|
|
3155
|
+
status: 401,
|
|
3156
|
+
request_id: requestId
|
|
3157
|
+
}),
|
|
3158
|
+
{ status: 401, headers }
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
if (attest.device && claims) attestedDevice = attest.device;
|
|
3162
|
+
attestChallengeOut = attest.nextChallenge;
|
|
2899
3163
|
const retryAfter = limiter.check(
|
|
2900
3164
|
meta.options?.rateLimit,
|
|
2901
3165
|
RateLimiter.key(hit.entry.id, userId, req.headers),
|
|
@@ -2915,8 +3179,6 @@ async function createApp(opts) {
|
|
|
2915
3179
|
}
|
|
2916
3180
|
let completionUploadId = null;
|
|
2917
3181
|
const args = [];
|
|
2918
|
-
let parsedBody;
|
|
2919
|
-
let bodyRead = false;
|
|
2920
3182
|
let sseEnqueue = null;
|
|
2921
3183
|
let sseSettle = async () => {
|
|
2922
3184
|
};
|
|
@@ -2927,10 +3189,7 @@ async function createApp(opts) {
|
|
|
2927
3189
|
for (const p of meta.params ?? []) {
|
|
2928
3190
|
switch (p.kind) {
|
|
2929
3191
|
case "body": {
|
|
2930
|
-
|
|
2931
|
-
parsedBody = await req.json().catch(() => ({}));
|
|
2932
|
-
bodyRead = true;
|
|
2933
|
-
}
|
|
3192
|
+
readParsedBody();
|
|
2934
3193
|
const r = p.schema.safeParse(parsedBody);
|
|
2935
3194
|
if (!r.success) {
|
|
2936
3195
|
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
@@ -2975,7 +3234,12 @@ async function createApp(opts) {
|
|
|
2975
3234
|
email: claims.email,
|
|
2976
3235
|
role: claims.role,
|
|
2977
3236
|
emailVerified: claims.email_verified === true,
|
|
2978
|
-
metadata: claims.metadata ?? {}
|
|
3237
|
+
metadata: claims.metadata ?? {},
|
|
3238
|
+
// Server-owned and only ever filled from a verdict palauth
|
|
3239
|
+
// returned. Never from a raw header, never from JWT metadata —
|
|
3240
|
+
// the type has said so since it was written and this is the
|
|
3241
|
+
// first writer.
|
|
3242
|
+
device: attestedDevice
|
|
2979
3243
|
} : null;
|
|
2980
3244
|
break;
|
|
2981
3245
|
case "uploadedObject": {
|
|
@@ -2987,10 +3251,7 @@ async function createApp(opts) {
|
|
|
2987
3251
|
requestId
|
|
2988
3252
|
);
|
|
2989
3253
|
}
|
|
2990
|
-
|
|
2991
|
-
parsedBody = await req.json().catch(() => ({}));
|
|
2992
|
-
bodyRead = true;
|
|
2993
|
-
}
|
|
3254
|
+
readParsedBody();
|
|
2994
3255
|
const envelopeIn = parsedBody;
|
|
2995
3256
|
if (!envelopeIn?.uploadedObject) {
|
|
2996
3257
|
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
@@ -3173,7 +3434,10 @@ async function createApp(opts) {
|
|
|
3173
3434
|
contentType: JSON_HEADERS["content-type"] ?? "application/json"
|
|
3174
3435
|
});
|
|
3175
3436
|
}
|
|
3176
|
-
return new Response(payload, {
|
|
3437
|
+
return new Response(payload, {
|
|
3438
|
+
status: 200,
|
|
3439
|
+
headers: attestChallengeOut ? { ...JSON_HEADERS, "x-palbase-attest-challenge": attestChallengeOut } : JSON_HEADERS
|
|
3440
|
+
});
|
|
3177
3441
|
} catch (err) {
|
|
3178
3442
|
await db.rollback(err);
|
|
3179
3443
|
if (isHttpError(err)) {
|