@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
package/dist/db/index.js
CHANGED
|
@@ -22,14 +22,14 @@ import {
|
|
|
22
22
|
userRef,
|
|
23
23
|
uuid,
|
|
24
24
|
vector
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-G4R6BTLV.js";
|
|
26
26
|
import {
|
|
27
27
|
TxPlanError,
|
|
28
28
|
TxRefError,
|
|
29
29
|
dec,
|
|
30
30
|
inc,
|
|
31
31
|
now
|
|
32
|
-
} from "../chunk-
|
|
32
|
+
} from "../chunk-CJSKYY76.js";
|
|
33
33
|
import "../chunk-7D4SUZUM.js";
|
|
34
34
|
export {
|
|
35
35
|
EXTENSION_DEPENDENCIES,
|
package/dist/engine/index.cjs
CHANGED
|
@@ -526,6 +526,20 @@ function makeTableProxy(ops, prefix) {
|
|
|
526
526
|
findById: (id) => ops().findById(name, id),
|
|
527
527
|
findMany: (query, opts) => ops().findMany(name, query, opts),
|
|
528
528
|
upsert: (data, opts) => ops().upsert(name, data, opts),
|
|
529
|
+
// THREE VERBS THE TYPE PROMISED AND THIS PROXY DID NOT EMIT.
|
|
530
|
+
//
|
|
531
|
+
// `EnvTypedTableBase` declares `updateMany`, `deleteMany` and `count`
|
|
532
|
+
// (typed-db.ts) and the ops layer implements all three — only this
|
|
533
|
+
// proxy, which is what a handler actually touches, left them out. So
|
|
534
|
+
// the type said the verb exists, autocomplete offered it, and the call
|
|
535
|
+
// answered `undefined is not a function`.
|
|
536
|
+
//
|
|
537
|
+
// Older than this run, but the run rewrote this proxy for
|
|
538
|
+
// `Database.schema(name).tables.*` and would have carried the gap onto
|
|
539
|
+
// the new surface too.
|
|
540
|
+
updateMany: (where, set) => ops().updateMany(name, where, set),
|
|
541
|
+
deleteMany: (where) => ops().deleteMany(name, where),
|
|
542
|
+
count: (where) => ops().count(name, where),
|
|
529
543
|
search: (params) => ops().search(name, params),
|
|
530
544
|
similar: (id, params) => ops().similar(name, id, params),
|
|
531
545
|
recommend: (params) => ops().recommend(name, params),
|
|
@@ -1127,6 +1141,11 @@ function assertUsableWriteValues(caller, table, cols, data) {
|
|
|
1127
1141
|
}
|
|
1128
1142
|
}
|
|
1129
1143
|
|
|
1144
|
+
// src/db/schema-json.ts
|
|
1145
|
+
function qualifiedTableKey(schemaName, tableName) {
|
|
1146
|
+
return schemaName === "" || schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1130
1149
|
// src/engine/db.ts
|
|
1131
1150
|
function quoteIdent(name) {
|
|
1132
1151
|
return `"${name.replace(/"/g, '""')}"`;
|
|
@@ -2289,9 +2308,6 @@ function setSchema(schemas) {
|
|
|
2289
2308
|
}
|
|
2290
2309
|
currentSchema = { tables };
|
|
2291
2310
|
}
|
|
2292
|
-
function qualifiedTableKey(schema, table) {
|
|
2293
|
-
return schema === "" || schema === "public" ? table : `${schema}.${table}`;
|
|
2294
|
-
}
|
|
2295
2311
|
function withTables(ops, schema = currentSchema) {
|
|
2296
2312
|
const bind = (key) => ({
|
|
2297
2313
|
insert: (data) => ops.insert(key, data),
|
|
@@ -2673,6 +2689,182 @@ function matchRoute(table, method, pathname) {
|
|
|
2673
2689
|
return null;
|
|
2674
2690
|
}
|
|
2675
2691
|
|
|
2692
|
+
// src/engine/attest.ts
|
|
2693
|
+
var HEADER_DEVICE_ID = "x-palbase-device-id";
|
|
2694
|
+
var HEADER_PAYLOAD = "x-palbase-attest-payload";
|
|
2695
|
+
var HEADER_SIGNATURE = "x-palbase-attest-signature";
|
|
2696
|
+
var HEADER_CHALLENGE = "x-palbase-attest-challenge";
|
|
2697
|
+
var HEADER_CAPABLE = "x-palbase-attest-capable";
|
|
2698
|
+
var HEADER_INSTALLATION = "x-palbase-installation";
|
|
2699
|
+
var HEADER_ANCHOR = "x-palbase-installation-anchor";
|
|
2700
|
+
var DEFAULT_POLICY_TTL_MS = 3e4;
|
|
2701
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 2e3;
|
|
2702
|
+
async function bodyDigest(body) {
|
|
2703
|
+
if (!body || body.byteLength === 0) return "";
|
|
2704
|
+
const bytes = new Uint8Array(new ArrayBuffer(body.byteLength));
|
|
2705
|
+
bytes.set(body);
|
|
2706
|
+
const buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
2707
|
+
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
2708
|
+
}
|
|
2709
|
+
function readCapable(req) {
|
|
2710
|
+
const v = req.headers.get(HEADER_CAPABLE);
|
|
2711
|
+
return v === "yes" || v === "no" ? v : "unknown";
|
|
2712
|
+
}
|
|
2713
|
+
function createAttestGate(deps) {
|
|
2714
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
2715
|
+
const ttl = deps.policyTtlMs ?? DEFAULT_POLICY_TTL_MS;
|
|
2716
|
+
const policyTimeoutMs = deps.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2717
|
+
let cachedMode = "off";
|
|
2718
|
+
let cachedAt = -Infinity;
|
|
2719
|
+
async function mode() {
|
|
2720
|
+
if (Date.now() - cachedAt < ttl) return cachedMode;
|
|
2721
|
+
try {
|
|
2722
|
+
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/attest-policy`, {
|
|
2723
|
+
headers: { apikey: deps.serviceRoleKey },
|
|
2724
|
+
signal: AbortSignal.timeout(policyTimeoutMs)
|
|
2725
|
+
});
|
|
2726
|
+
if (!res.ok) throw new Error(`attest-policy ${res.status}`);
|
|
2727
|
+
const body = await res.json();
|
|
2728
|
+
cachedMode = body.mode === "monitor" || body.mode === "enforce" ? body.mode : "off";
|
|
2729
|
+
cachedAt = Date.now();
|
|
2730
|
+
} catch {
|
|
2731
|
+
}
|
|
2732
|
+
return cachedMode;
|
|
2733
|
+
}
|
|
2734
|
+
return async function gate(req, body, _ctx) {
|
|
2735
|
+
const m = await mode();
|
|
2736
|
+
if (m === "off") {
|
|
2737
|
+
return { outcome: "pass", reason: "", device: null, observation: null, nextChallenge: null };
|
|
2738
|
+
}
|
|
2739
|
+
const deviceId = req.headers.get(HEADER_DEVICE_ID) ?? "";
|
|
2740
|
+
const payload = req.headers.get(HEADER_PAYLOAD) ?? "";
|
|
2741
|
+
const signature = req.headers.get(HEADER_SIGNATURE) ?? "";
|
|
2742
|
+
const hasEvidence = deviceId !== "" && payload !== "" && signature !== "";
|
|
2743
|
+
const observation = {
|
|
2744
|
+
capable: readCapable(req),
|
|
2745
|
+
attested: false,
|
|
2746
|
+
installationId: req.headers.get(HEADER_INSTALLATION) || null
|
|
2747
|
+
};
|
|
2748
|
+
if (m === "monitor") {
|
|
2749
|
+
observation.attested = hasEvidence;
|
|
2750
|
+
return { outcome: "pass", reason: "", device: null, observation, nextChallenge: null };
|
|
2751
|
+
}
|
|
2752
|
+
const url = new URL(req.url);
|
|
2753
|
+
let verdict;
|
|
2754
|
+
try {
|
|
2755
|
+
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/assert`, {
|
|
2756
|
+
method: "POST",
|
|
2757
|
+
headers: { apikey: deps.serviceRoleKey, "content-type": "application/json" },
|
|
2758
|
+
// A HUNG palauth must not become a hung tenant. Without this, a
|
|
2759
|
+
// process that accepts the connection and never answers holds every
|
|
2760
|
+
// request open until the client gives up — the gate would take the
|
|
2761
|
+
// whole stack down while claiming to protect it.
|
|
2762
|
+
signal: AbortSignal.timeout(policyTimeoutMs),
|
|
2763
|
+
body: JSON.stringify({
|
|
2764
|
+
installation_id: req.headers.get(HEADER_ANCHOR) ?? "",
|
|
2765
|
+
device_id: deviceId,
|
|
2766
|
+
challenge: req.headers.get(HEADER_CHALLENGE) ?? "",
|
|
2767
|
+
payload,
|
|
2768
|
+
signature,
|
|
2769
|
+
method: req.method,
|
|
2770
|
+
// pathname + search, NOT pathname alone.
|
|
2771
|
+
//
|
|
2772
|
+
// The client signs the path string it was given, and that string can
|
|
2773
|
+
// carry a query — the transport splits and reattaches one on purpose.
|
|
2774
|
+
// Recomputing from pathname alone would drop the query from the
|
|
2775
|
+
// signed definition, which breaks it BOTH ways: an attested call
|
|
2776
|
+
// carrying a query would fail to verify, and a captured assertion
|
|
2777
|
+
// could be replayed against the same route with different query
|
|
2778
|
+
// parameters, which is exactly the substitution the binding exists to
|
|
2779
|
+
// prevent.
|
|
2780
|
+
path: url.pathname + url.search,
|
|
2781
|
+
body_sha256: await bodyDigest(body)
|
|
2782
|
+
})
|
|
2783
|
+
});
|
|
2784
|
+
if (!res.ok) throw new Error(`assert ${res.status}`);
|
|
2785
|
+
verdict = await res.json();
|
|
2786
|
+
} catch {
|
|
2787
|
+
return {
|
|
2788
|
+
outcome: "reject",
|
|
2789
|
+
reason: "app_attest_required",
|
|
2790
|
+
device: null,
|
|
2791
|
+
observation,
|
|
2792
|
+
nextChallenge: null
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
if (!verdict.ok) {
|
|
2796
|
+
const reason2 = verdict.reason === "assertion_invalid" || verdict.reason === "assertion_stale" ? verdict.reason : "app_attest_required";
|
|
2797
|
+
return {
|
|
2798
|
+
outcome: "reject",
|
|
2799
|
+
reason: reason2,
|
|
2800
|
+
device: null,
|
|
2801
|
+
observation,
|
|
2802
|
+
nextChallenge: verdict.next_challenge || null
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
observation.attested = true;
|
|
2806
|
+
return {
|
|
2807
|
+
outcome: "pass",
|
|
2808
|
+
reason: "",
|
|
2809
|
+
device: {
|
|
2810
|
+
installationId: "",
|
|
2811
|
+
authDeviceId: deviceId,
|
|
2812
|
+
platform: "ios",
|
|
2813
|
+
assurance: "attested"
|
|
2814
|
+
},
|
|
2815
|
+
observation,
|
|
2816
|
+
nextChallenge: verdict.next_challenge || null
|
|
2817
|
+
};
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function createObservationSink(deps) {
|
|
2821
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
2822
|
+
const maxBatch = deps.maxBatch ?? 1e3;
|
|
2823
|
+
const intervalMs = deps.intervalMs ?? 1e4;
|
|
2824
|
+
let buffer = [];
|
|
2825
|
+
let inFlight = Promise.resolve();
|
|
2826
|
+
const timer = setInterval(() => {
|
|
2827
|
+
void flush();
|
|
2828
|
+
}, intervalMs);
|
|
2829
|
+
timer.unref?.();
|
|
2830
|
+
function bucketsOf(batch) {
|
|
2831
|
+
const counts = { capable: 0, incapable: 0, unknown: 0, no_sdk: 0 };
|
|
2832
|
+
for (const o of batch) {
|
|
2833
|
+
if (o.attested || o.capable === "yes") counts.capable += 1;
|
|
2834
|
+
else if (o.capable === "no") counts.incapable += 1;
|
|
2835
|
+
else if (o.installationId) counts.unknown += 1;
|
|
2836
|
+
else counts.no_sdk += 1;
|
|
2837
|
+
}
|
|
2838
|
+
return counts;
|
|
2839
|
+
}
|
|
2840
|
+
async function flush() {
|
|
2841
|
+
if (buffer.length === 0) return;
|
|
2842
|
+
const batch = buffer;
|
|
2843
|
+
buffer = [];
|
|
2844
|
+
inFlight = (async () => {
|
|
2845
|
+
try {
|
|
2846
|
+
await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/observations`, {
|
|
2847
|
+
method: "POST",
|
|
2848
|
+
headers: { apikey: deps.serviceRoleKey, "content-type": "application/json" },
|
|
2849
|
+
body: JSON.stringify(bucketsOf(batch))
|
|
2850
|
+
});
|
|
2851
|
+
} catch {
|
|
2852
|
+
}
|
|
2853
|
+
})();
|
|
2854
|
+
await inFlight;
|
|
2855
|
+
}
|
|
2856
|
+
return {
|
|
2857
|
+
record(o) {
|
|
2858
|
+
buffer.push(o);
|
|
2859
|
+
if (buffer.length >= maxBatch) void flush();
|
|
2860
|
+
},
|
|
2861
|
+
flush,
|
|
2862
|
+
stop() {
|
|
2863
|
+
clearInterval(timer);
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2676
2868
|
// src/engine/upload.ts
|
|
2677
2869
|
var AUTHORIZE_PATH = "/__palbase/upload/authorize";
|
|
2678
2870
|
var SIGNATURE_HEADER = "x-palbase-upload-signature";
|
|
@@ -2826,6 +3018,7 @@ function installEgressFence(policy) {
|
|
|
2826
3018
|
}
|
|
2827
3019
|
|
|
2828
3020
|
// src/engine/index.ts
|
|
3021
|
+
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
2829
3022
|
var JSON_HEADERS = { "content-type": "application/json" };
|
|
2830
3023
|
function fieldErrors(err) {
|
|
2831
3024
|
return err.issues.map((i) => ({ field: i.path.join("."), message: i.message }));
|
|
@@ -2886,6 +3079,14 @@ async function createApp(opts) {
|
|
|
2886
3079
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
2887
3080
|
await sql.unsafe("select 1");
|
|
2888
3081
|
const auth = new AuthVerifier({ jwksUrl: config.authJwksUrl, issuer: config.authIssuer });
|
|
3082
|
+
const attestGate = createAttestGate({
|
|
3083
|
+
moduleBaseUrl: config.moduleBaseUrl,
|
|
3084
|
+
serviceRoleKey: config.serviceRoleKey
|
|
3085
|
+
});
|
|
3086
|
+
const observations = createObservationSink({
|
|
3087
|
+
moduleBaseUrl: config.moduleBaseUrl,
|
|
3088
|
+
serviceRoleKey: config.serviceRoleKey
|
|
3089
|
+
});
|
|
2889
3090
|
const limiter = new RateLimiter();
|
|
2890
3091
|
const cache = opts.cache ?? makeMemoryCache();
|
|
2891
3092
|
const log = opts.logger ?? console;
|
|
@@ -2967,6 +3168,8 @@ async function createApp(opts) {
|
|
|
2967
3168
|
const hit = matchRoute(routes, req.method, url.pathname);
|
|
2968
3169
|
if (!hit) return envelope("not_found", "No route matches this method and path", 404, requestId);
|
|
2969
3170
|
const { meta } = hit.entry;
|
|
3171
|
+
let attestedDevice = null;
|
|
3172
|
+
let attestChallengeOut = null;
|
|
2970
3173
|
const spec = effectiveAuth(meta.options?.auth, hit.entry.controllerAuth);
|
|
2971
3174
|
const claims = await auth.verify(req.headers.get("authorization"));
|
|
2972
3175
|
if (spec.required && !claims) {
|
|
@@ -2979,6 +3182,67 @@ async function createApp(opts) {
|
|
|
2979
3182
|
if (claims && spec.verifiedEmail && claims.email_verified !== true) {
|
|
2980
3183
|
return envelope("email_not_verified", "A verified email address is required", 403, requestId);
|
|
2981
3184
|
}
|
|
3185
|
+
let parsedBody;
|
|
3186
|
+
let bodyRead = false;
|
|
3187
|
+
let rawBody = null;
|
|
3188
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
3189
|
+
const declared = Number(req.headers.get("content-length") ?? "");
|
|
3190
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
3191
|
+
return new Response(
|
|
3192
|
+
JSON.stringify({
|
|
3193
|
+
error: "payload_too_large",
|
|
3194
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
3195
|
+
status: 413,
|
|
3196
|
+
request_id: requestId
|
|
3197
|
+
}),
|
|
3198
|
+
{ status: 413, headers: JSON_HEADERS }
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
const buf = await req.arrayBuffer().catch(() => null);
|
|
3202
|
+
if (buf && buf.byteLength > MAX_BODY_BYTES) {
|
|
3203
|
+
return new Response(
|
|
3204
|
+
JSON.stringify({
|
|
3205
|
+
error: "payload_too_large",
|
|
3206
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
3207
|
+
status: 413,
|
|
3208
|
+
request_id: requestId
|
|
3209
|
+
}),
|
|
3210
|
+
{ status: 413, headers: JSON_HEADERS }
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
|
|
3214
|
+
}
|
|
3215
|
+
const readParsedBody = () => {
|
|
3216
|
+
if (bodyRead) return parsedBody;
|
|
3217
|
+
bodyRead = true;
|
|
3218
|
+
if (!rawBody) {
|
|
3219
|
+
parsedBody = {};
|
|
3220
|
+
return parsedBody;
|
|
3221
|
+
}
|
|
3222
|
+
try {
|
|
3223
|
+
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
3224
|
+
} catch {
|
|
3225
|
+
parsedBody = {};
|
|
3226
|
+
}
|
|
3227
|
+
return parsedBody;
|
|
3228
|
+
};
|
|
3229
|
+
const attest = await attestGate(req, rawBody, { userId });
|
|
3230
|
+
if (attest.observation) observations.record(attest.observation);
|
|
3231
|
+
if (attest.outcome === "reject") {
|
|
3232
|
+
const headers = { ...JSON_HEADERS };
|
|
3233
|
+
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
3234
|
+
return new Response(
|
|
3235
|
+
JSON.stringify({
|
|
3236
|
+
error: attest.reason,
|
|
3237
|
+
error_description: "This endpoint requires a verified app instance",
|
|
3238
|
+
status: 401,
|
|
3239
|
+
request_id: requestId
|
|
3240
|
+
}),
|
|
3241
|
+
{ status: 401, headers }
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
if (attest.device && claims) attestedDevice = attest.device;
|
|
3245
|
+
attestChallengeOut = attest.nextChallenge;
|
|
2982
3246
|
const retryAfter = limiter.check(
|
|
2983
3247
|
meta.options?.rateLimit,
|
|
2984
3248
|
RateLimiter.key(hit.entry.id, userId, req.headers),
|
|
@@ -2998,8 +3262,6 @@ async function createApp(opts) {
|
|
|
2998
3262
|
}
|
|
2999
3263
|
let completionUploadId = null;
|
|
3000
3264
|
const args = [];
|
|
3001
|
-
let parsedBody;
|
|
3002
|
-
let bodyRead = false;
|
|
3003
3265
|
let sseEnqueue = null;
|
|
3004
3266
|
let sseSettle = async () => {
|
|
3005
3267
|
};
|
|
@@ -3010,10 +3272,7 @@ async function createApp(opts) {
|
|
|
3010
3272
|
for (const p of meta.params ?? []) {
|
|
3011
3273
|
switch (p.kind) {
|
|
3012
3274
|
case "body": {
|
|
3013
|
-
|
|
3014
|
-
parsedBody = await req.json().catch(() => ({}));
|
|
3015
|
-
bodyRead = true;
|
|
3016
|
-
}
|
|
3275
|
+
readParsedBody();
|
|
3017
3276
|
const r = p.schema.safeParse(parsedBody);
|
|
3018
3277
|
if (!r.success) {
|
|
3019
3278
|
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
@@ -3058,7 +3317,12 @@ async function createApp(opts) {
|
|
|
3058
3317
|
email: claims.email,
|
|
3059
3318
|
role: claims.role,
|
|
3060
3319
|
emailVerified: claims.email_verified === true,
|
|
3061
|
-
metadata: claims.metadata ?? {}
|
|
3320
|
+
metadata: claims.metadata ?? {},
|
|
3321
|
+
// Server-owned and only ever filled from a verdict palauth
|
|
3322
|
+
// returned. Never from a raw header, never from JWT metadata —
|
|
3323
|
+
// the type has said so since it was written and this is the
|
|
3324
|
+
// first writer.
|
|
3325
|
+
device: attestedDevice
|
|
3062
3326
|
} : null;
|
|
3063
3327
|
break;
|
|
3064
3328
|
case "uploadedObject": {
|
|
@@ -3070,10 +3334,7 @@ async function createApp(opts) {
|
|
|
3070
3334
|
requestId
|
|
3071
3335
|
);
|
|
3072
3336
|
}
|
|
3073
|
-
|
|
3074
|
-
parsedBody = await req.json().catch(() => ({}));
|
|
3075
|
-
bodyRead = true;
|
|
3076
|
-
}
|
|
3337
|
+
readParsedBody();
|
|
3077
3338
|
const envelopeIn = parsedBody;
|
|
3078
3339
|
if (!envelopeIn?.uploadedObject) {
|
|
3079
3340
|
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
@@ -3256,7 +3517,10 @@ async function createApp(opts) {
|
|
|
3256
3517
|
contentType: JSON_HEADERS["content-type"] ?? "application/json"
|
|
3257
3518
|
});
|
|
3258
3519
|
}
|
|
3259
|
-
return new Response(payload, {
|
|
3520
|
+
return new Response(payload, {
|
|
3521
|
+
status: 200,
|
|
3522
|
+
headers: attestChallengeOut ? { ...JSON_HEADERS, "x-palbase-attest-challenge": attestChallengeOut } : JSON_HEADERS
|
|
3523
|
+
});
|
|
3260
3524
|
} catch (err) {
|
|
3261
3525
|
await db.rollback(err);
|
|
3262
3526
|
if (isHttpError(err)) {
|