@opendatalabs/personal-server-ts-core 1.5.0 → 1.7.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/api/index.d.ts +41 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +298 -87
- package/dist/api/index.js.map +1 -1
- package/dist/contracts/binary.d.ts +12 -0
- package/dist/contracts/binary.d.ts.map +1 -1
- package/dist/contracts/binary.js +31 -0
- package/dist/contracts/binary.js.map +1 -1
- package/dist/contracts/data.d.ts +35 -0
- package/dist/contracts/data.d.ts.map +1 -1
- package/dist/contracts/data.js +96 -14
- package/dist/contracts/data.js.map +1 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/errors/catalog.d.ts +34 -0
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/catalog.js +41 -0
- package/dist/errors/catalog.js.map +1 -1
- package/dist/lineage/attestation.d.ts +42 -0
- package/dist/lineage/attestation.d.ts.map +1 -0
- package/dist/lineage/attestation.js +25 -0
- package/dist/lineage/attestation.js.map +1 -0
- package/dist/lineage/gateway.d.ts +105 -0
- package/dist/lineage/gateway.d.ts.map +1 -0
- package/dist/lineage/gateway.js +242 -0
- package/dist/lineage/gateway.js.map +1 -0
- package/dist/lineage/index.d.ts +5 -0
- package/dist/lineage/index.d.ts.map +1 -0
- package/dist/lineage/index.js +5 -0
- package/dist/lineage/index.js.map +1 -0
- package/dist/lineage/lineage.d.ts +141 -0
- package/dist/lineage/lineage.d.ts.map +1 -0
- package/dist/lineage/lineage.js +255 -0
- package/dist/lineage/lineage.js.map +1 -0
- package/dist/logging/access-log.d.ts +1 -1
- package/dist/logging/access-log.d.ts.map +1 -1
- package/dist/policy/data-read.d.ts +8 -0
- package/dist/policy/data-read.d.ts.map +1 -1
- package/dist/policy/data-read.js +9 -2
- package/dist/policy/data-read.js.map +1 -1
- package/dist/policy/data-write.d.ts +69 -0
- package/dist/policy/data-write.d.ts.map +1 -0
- package/dist/policy/data-write.js +131 -0
- package/dist/policy/data-write.js.map +1 -0
- package/dist/policy/index.d.ts +2 -1
- package/dist/policy/index.d.ts.map +1 -1
- package/dist/policy/index.js +2 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/signing/request-signer.d.ts +2 -0
- package/dist/signing/request-signer.d.ts.map +1 -1
- package/dist/signing/request-signer.js +1 -0
- package/dist/signing/request-signer.js.map +1 -1
- package/dist/signing/signer.d.ts +8 -0
- package/dist/signing/signer.d.ts.map +1 -1
- package/dist/signing/signer.js +9 -0
- package/dist/signing/signer.js.map +1 -1
- package/dist/sync/data-point-id.d.ts +7 -0
- package/dist/sync/data-point-id.d.ts.map +1 -0
- package/dist/sync/data-point-id.js +16 -0
- package/dist/sync/data-point-id.js.map +1 -0
- package/dist/sync/workers/upload.d.ts +10 -6
- package/dist/sync/workers/upload.d.ts.map +1 -1
- package/dist/sync/workers/upload.js +39 -17
- package/dist/sync/workers/upload.js.map +1 -1
- package/dist/write/attribution.d.ts +167 -0
- package/dist/write/attribution.d.ts.map +1 -0
- package/dist/write/attribution.js +325 -0
- package/dist/write/attribution.js.map +1 -0
- package/dist/write/index.d.ts +3 -0
- package/dist/write/index.d.ts.map +1 -0
- package/dist/write/index.js +3 -0
- package/dist/write/index.js.map +1 -0
- package/dist/write/session.d.ts +96 -0
- package/dist/write/session.d.ts.map +1 -0
- package/dist/write/session.js +136 -0
- package/dist/write/session.js.map +1 -0
- package/package.json +9 -1
package/dist/api/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { SyncManager } from "../sync/index.js";
|
|
|
6
6
|
import { type OAuthDeviceSessionLookup, type OAuthTokenStorePort } from "../contracts/index.js";
|
|
7
7
|
import type { DataPortabilityGatewayConfig, GatewayClient } from "@opendatalabs/vana-sdk/browser";
|
|
8
8
|
import type { ServerSigner } from "../signing/index.js";
|
|
9
|
+
import type { WriterAttribution } from "../write/attribution.js";
|
|
10
|
+
import type { LineageGatewayPort } from "../lineage/gateway.js";
|
|
9
11
|
import { type X402Challenge } from "../payment/index.js";
|
|
10
12
|
export interface PersonalServerReadAuthInput {
|
|
11
13
|
request: Request;
|
|
@@ -23,6 +25,28 @@ export interface PersonalServerReadAuthResult {
|
|
|
23
25
|
builder?: `0x${string}` | string;
|
|
24
26
|
grantId?: string;
|
|
25
27
|
}
|
|
28
|
+
export interface PersonalServerWriteAuthInput {
|
|
29
|
+
request: Request;
|
|
30
|
+
/** Raw scope path param (validated by parseDataScopeContract after auth,
|
|
31
|
+
* same precedence as the owner write path). */
|
|
32
|
+
scope: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Result of authorizing a DELEGATED (write-session) write. A void return
|
|
36
|
+
* means the request was authorized as the owner instead — the ingest then
|
|
37
|
+
* proceeds exactly as today, with no attribution stamped.
|
|
38
|
+
*/
|
|
39
|
+
export interface PersonalServerWriteAuthResult {
|
|
40
|
+
builder: `0x${string}`;
|
|
41
|
+
grantId: string;
|
|
42
|
+
attribution: WriterAttribution;
|
|
43
|
+
/**
|
|
44
|
+
* Rolls back the per-write proof reservation (replay guard). The handler
|
|
45
|
+
* calls it when the write fails BEFORE the record is committed, so the
|
|
46
|
+
* builder can retry with the same still-valid proof.
|
|
47
|
+
*/
|
|
48
|
+
releaseProof?: () => Promise<void>;
|
|
49
|
+
}
|
|
26
50
|
export interface PersonalServerReadFulfillment {
|
|
27
51
|
builder: string;
|
|
28
52
|
fileId?: string;
|
|
@@ -40,6 +64,16 @@ export interface PersonalServerApiAuthPort {
|
|
|
40
64
|
authorizeOwner(request: Request): Promise<void>;
|
|
41
65
|
authorizeBuilderList(request: Request): Promise<void>;
|
|
42
66
|
authorizeBuilderRead(input: PersonalServerReadAuthInput): Promise<PersonalServerReadAuthResult | void>;
|
|
67
|
+
/**
|
|
68
|
+
* Authorize a data ingest (POST /v1/data/:scope). Optional — auth ports
|
|
69
|
+
* that don't support delegated writes omit it and the handler falls back
|
|
70
|
+
* to authorizeOwner, preserving today's owner-only behavior. Ports that DO
|
|
71
|
+
* support write sessions handle BOTH paths here: a recognized session
|
|
72
|
+
* token authorizes as the builder (write policy + attribution proof) and
|
|
73
|
+
* returns the attribution to store; anything else falls through to the
|
|
74
|
+
* owner path and returns void.
|
|
75
|
+
*/
|
|
76
|
+
authorizeWrite?(input: PersonalServerWriteAuthInput): Promise<PersonalServerWriteAuthResult | void>;
|
|
43
77
|
}
|
|
44
78
|
export interface PersonalServerApiLogger {
|
|
45
79
|
debug?(payload: Record<string, unknown>, message: string): void;
|
|
@@ -122,6 +156,13 @@ export interface PersonalServerDataApiDeps {
|
|
|
122
156
|
now?: () => Date;
|
|
123
157
|
createLogId?: () => string;
|
|
124
158
|
logger?: PersonalServerApiLogger;
|
|
159
|
+
/**
|
|
160
|
+
* Gateway access for derivative data (docs/derivative-data-api.md): source
|
|
161
|
+
* lookups for lineage validation on write and the signed lineage read
|
|
162
|
+
* behind GET /v1/data/:scope/lineage. Absent = writes with lineage can
|
|
163
|
+
* only cite local scopes and the lineage read answers 503.
|
|
164
|
+
*/
|
|
165
|
+
lineageGateway?: LineageGatewayPort;
|
|
125
166
|
}
|
|
126
167
|
export interface PersonalServerAccessLogsApiDeps {
|
|
127
168
|
auth: Pick<PersonalServerApiAuthPort, "authorizeOwner">;
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
|
package/dist/api/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { ProtocolError } from "../errors/catalog.js";
|
|
2
|
-
import { deleteDataScopeContract, ingestDataContract, ingestBinaryDataContract, listAccessLogsContract, listDataScopesContract, listDataVersionsContract, parseDataScopeContract, parseJsonObjectBody, readDataContract, syncFileContract, triggerSyncContract, getSyncStatusContract, configReadErrorContract, configWriteErrorContract, createGrantContract, listGrantsContract, oauthTokenContract, revokeGrantContract, validateServerConfigContract, verifyGrantContract, decodeBinaryEnvelope, isBinaryEnvelope, parseMetadataHeader, stringifyMetadataHeader, } from "../contracts/index.js";
|
|
1
|
+
import { GrantRequiredError, InvalidCascadeError, LineageCascadeUnavailableError, LineageGatewayError, LineageUnavailableError, ProtocolError, ServerNotConfiguredError, } from "../errors/catalog.js";
|
|
2
|
+
import { deleteDataScopeContract, ingestDataContract, ingestBinaryDataContract, listAccessLogsContract, listDataScopesContract, listDataVersionsContract, parseDataScopeContract, parseJsonObjectBody, readDataContract, syncFileContract, triggerSyncContract, getSyncStatusContract, configReadErrorContract, configWriteErrorContract, createGrantContract, listGrantsContract, oauthTokenContract, revokeGrantContract, validateServerConfigContract, verifyGrantContract, decodeBinaryEnvelope, isBinaryEnvelope, parseMetadataHeader, stringifyMetadataHeader, IngestPersistedError, } from "../contracts/index.js";
|
|
3
|
+
import { extractLineageField, prepareLineage, } from "../lineage/lineage.js";
|
|
4
|
+
import { computeDataPointId } from "../sync/data-point-id.js";
|
|
5
|
+
import { binaryFilename, binaryMimeType, isJsonContentType, } from "../contracts/binary.js";
|
|
3
6
|
import { buildChallenge, parsePaymentHeader, verifyPayment, } from "../payment/index.js";
|
|
4
7
|
function jsonResponse(body, init) {
|
|
5
8
|
const headers = new Headers(init?.headers);
|
|
@@ -249,30 +252,6 @@ function collectedAt(now) {
|
|
|
249
252
|
.toISOString()
|
|
250
253
|
.replace(/\.\d{3}Z$/, "Z");
|
|
251
254
|
}
|
|
252
|
-
/** True when the request body should be treated as a JSON object (the legacy
|
|
253
|
-
* path). Missing/blank Content-Type is treated as JSON for backward compat. */
|
|
254
|
-
function isJsonContentType(request) {
|
|
255
|
-
const ct = request.headers.get("content-type");
|
|
256
|
-
if (!ct)
|
|
257
|
-
return true;
|
|
258
|
-
return ct.toLowerCase().includes("application/json");
|
|
259
|
-
}
|
|
260
|
-
function binaryMimeType(request) {
|
|
261
|
-
const ct = request.headers.get("content-type");
|
|
262
|
-
if (!ct)
|
|
263
|
-
return "application/octet-stream";
|
|
264
|
-
// Strip any "; charset=..." / boundary parameters.
|
|
265
|
-
return ct.split(";")[0].trim() || "application/octet-stream";
|
|
266
|
-
}
|
|
267
|
-
/** Extract a filename from X-Filename or a Content-Disposition header. */
|
|
268
|
-
function binaryFilename(request) {
|
|
269
|
-
const explicit = request.headers.get("x-filename");
|
|
270
|
-
if (explicit)
|
|
271
|
-
return explicit;
|
|
272
|
-
const disposition = request.headers.get("content-disposition");
|
|
273
|
-
const match = disposition?.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/i);
|
|
274
|
-
return match ? decodeURIComponent(match[1]) : undefined;
|
|
275
|
-
}
|
|
276
255
|
function notifyNewData(syncManager) {
|
|
277
256
|
if (!syncManager)
|
|
278
257
|
return;
|
|
@@ -306,6 +285,73 @@ export function reportPersonalServerReadFulfillment(deps, event) {
|
|
|
306
285
|
warnReadFulfillmentReporterFailed(deps, event, err);
|
|
307
286
|
}
|
|
308
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Validate a write's caller-supplied `lineage` (JSON body top level, or the
|
|
290
|
+
* binary metadata object) into the `$lineage` record to stamp. `undefined`
|
|
291
|
+
* (or null) means a root record. Throws ProtocolErrors; the write handler
|
|
292
|
+
* lets them propagate before anything is stored.
|
|
293
|
+
*/
|
|
294
|
+
async function prepareWriteLineage(deps, scope, field) {
|
|
295
|
+
if (field === undefined || field === null)
|
|
296
|
+
return undefined;
|
|
297
|
+
return prepareLineage({
|
|
298
|
+
scope,
|
|
299
|
+
field,
|
|
300
|
+
serverOwner: deps.serverOwner,
|
|
301
|
+
storage: deps.storage,
|
|
302
|
+
gateway: deps.lineageGateway,
|
|
303
|
+
now: deps.now ?? (() => new Date()),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The lineage view a read auth result entitles the caller to. The auth port
|
|
308
|
+
* answers an owner read with no result or with the "owner" / "policy-bypass"
|
|
309
|
+
* sentinels (see api-auth): those get the full view. A builder read carries
|
|
310
|
+
* the grant the read policy resolved, and that grant names the view. A
|
|
311
|
+
* builder result with no resolved grant (or an "unknown" one) proves
|
|
312
|
+
* nothing and is refused: a missing grantId must never widen to the full
|
|
313
|
+
* view.
|
|
314
|
+
*/
|
|
315
|
+
function resolveLineageGrantView(authResult) {
|
|
316
|
+
if (authResult === undefined ||
|
|
317
|
+
authResult.grantId === "owner" ||
|
|
318
|
+
authResult.grantId === "policy-bypass") {
|
|
319
|
+
return { grantId: undefined };
|
|
320
|
+
}
|
|
321
|
+
const grantId = authResult.grantId;
|
|
322
|
+
if (typeof grantId !== "string" || grantId === "" || grantId === "unknown") {
|
|
323
|
+
throw new GrantRequiredError({
|
|
324
|
+
reason: "a lineage read by a builder needs a resolved grant",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return { grantId };
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Delete one scope the way DELETE /v1/data/:scope always has: propagate to
|
|
331
|
+
* the authoritative stores first, best-effort (on DPv2 the remote step is a
|
|
332
|
+
* logged no-op until the tombstone-based delete lands), then drop the local
|
|
333
|
+
* copy.
|
|
334
|
+
*/
|
|
335
|
+
async function deleteOneScope(deps, scope) {
|
|
336
|
+
if (deps.syncManager?.deleteScopeRemote) {
|
|
337
|
+
try {
|
|
338
|
+
await deps.syncManager.deleteScopeRemote(scope);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
deps.logger?.info?.({ scope, error: err.message }, "Remote scope deletion failed; proceeding with local delete");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const result = await deleteDataScopeContract({
|
|
345
|
+
storage: deps.storage,
|
|
346
|
+
scopeParam: scope,
|
|
347
|
+
});
|
|
348
|
+
if (!result.ok) {
|
|
349
|
+
// The scope was validated by the caller; a contract error here is a bug.
|
|
350
|
+
throw new ProtocolError(result.status, result.body.error, result.body.message);
|
|
351
|
+
}
|
|
352
|
+
deps.logger?.info?.({ scope, deletedCount: result.deletedCount }, "Scope deleted");
|
|
353
|
+
return result.deletedCount;
|
|
354
|
+
}
|
|
309
355
|
export async function handlePersonalServerDataRequest(request, deps, options = {}) {
|
|
310
356
|
return withApiErrors(async () => {
|
|
311
357
|
const url = new URL(request.url);
|
|
@@ -337,6 +383,77 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
337
383
|
return contractErrorResponse(result);
|
|
338
384
|
return jsonResponse(result.response);
|
|
339
385
|
}
|
|
386
|
+
if ((parts.length === 2 || parts.length === 3) && parts[1] === "lineage") {
|
|
387
|
+
if (request.method !== "GET")
|
|
388
|
+
return methodNotAllowed();
|
|
389
|
+
const scopeResult = parseDataScopeContract(decodePathPart(parts[0]));
|
|
390
|
+
if (!scopeResult.ok)
|
|
391
|
+
return contractErrorResponse(scopeResult);
|
|
392
|
+
// The version is a PATH segment (/v1/data/:scope/lineage/:version), so
|
|
393
|
+
// it is inside the signed request uri: a builder's signature for one
|
|
394
|
+
// version cannot be replayed to read another. The grant view is the
|
|
395
|
+
// signed grantId claim the read auth already honours. Query
|
|
396
|
+
// parameters would be unsigned inputs to the view and are refused.
|
|
397
|
+
if (url.searchParams.has("version")) {
|
|
398
|
+
return errorResponse(400, "INVALID_VERSION", "version is a path segment (/v1/data/:scope/lineage/:version), not a query parameter");
|
|
399
|
+
}
|
|
400
|
+
if (url.search.length > 0) {
|
|
401
|
+
return errorResponse(400, "INVALID_QUERY", "lineage reads take no query parameters; the version is a path segment and the grant view is the signed grantId claim");
|
|
402
|
+
}
|
|
403
|
+
const version = parts.length === 3 ? decodePathPart(parts[2]) : undefined;
|
|
404
|
+
if (version !== undefined && !/^[1-9]\d*$/.test(version)) {
|
|
405
|
+
return errorResponse(400, "INVALID_VERSION", "version must be a positive decimal integer");
|
|
406
|
+
}
|
|
407
|
+
// Same gate as a read of the scope: the owner, or a builder whose grant
|
|
408
|
+
// covers it. The gateway then attests the view that grant sees, so a
|
|
409
|
+
// builder only learns the scopes of nodes its grant covers.
|
|
410
|
+
const authResult = await deps.auth.authorizeBuilderRead({
|
|
411
|
+
request,
|
|
412
|
+
scope: scopeResult.scope,
|
|
413
|
+
grantId: selectedGrantId(request, url),
|
|
414
|
+
});
|
|
415
|
+
if (!deps.serverOwner) {
|
|
416
|
+
throw new ServerNotConfiguredError({
|
|
417
|
+
reason: "serverOwner is required to resolve the data point id",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (!deps.lineageGateway)
|
|
421
|
+
throw new LineageUnavailableError();
|
|
422
|
+
const { grantId } = resolveLineageGrantView(authResult);
|
|
423
|
+
let result;
|
|
424
|
+
try {
|
|
425
|
+
result = await deps.lineageGateway.getLineage({
|
|
426
|
+
dataPointId: computeDataPointId(deps.serverOwner, scopeResult.scope),
|
|
427
|
+
version,
|
|
428
|
+
grantId,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
// Transport failures (DNS, refused, timeout) are gateway errors to
|
|
433
|
+
// the caller, not internal ones: same 502 as a gateway error body.
|
|
434
|
+
if (err instanceof ProtocolError)
|
|
435
|
+
throw err;
|
|
436
|
+
throw new LineageGatewayError({
|
|
437
|
+
status: 0,
|
|
438
|
+
body: { error: err instanceof Error ? err.message : String(err) },
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (!result.ok) {
|
|
442
|
+
if (result.status === 404) {
|
|
443
|
+
return errorResponse(404, "NOT_FOUND", version
|
|
444
|
+
? `Scope "${scopeResult.scope}" has no registered version ${version}`
|
|
445
|
+
: `Scope "${scopeResult.scope}" is not registered at the gateway`);
|
|
446
|
+
}
|
|
447
|
+
if (result.status === 403) {
|
|
448
|
+
throw new ProtocolError(403, "LINEAGE_FORBIDDEN", "The gateway refused the lineage view for this grant", { gateway: result.body });
|
|
449
|
+
}
|
|
450
|
+
throw new LineageGatewayError({
|
|
451
|
+
status: result.status,
|
|
452
|
+
body: result.body,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return jsonResponse({ data: result.data, proof: result.proof });
|
|
456
|
+
}
|
|
340
457
|
if (parts.length !== 1)
|
|
341
458
|
return notFound();
|
|
342
459
|
const scopeParam = decodePathPart(parts[0]);
|
|
@@ -527,85 +644,179 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
527
644
|
return response;
|
|
528
645
|
}
|
|
529
646
|
if (request.method === "POST") {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
647
|
+
// Delegated writes: an auth port that supports write sessions handles
|
|
648
|
+
// both paths in authorizeWrite (builder session token -> write policy +
|
|
649
|
+
// attribution; anything else -> owner path). Ports without it keep
|
|
650
|
+
// today's owner-only gate.
|
|
651
|
+
let writeAuth;
|
|
652
|
+
if (deps.auth.authorizeWrite) {
|
|
653
|
+
writeAuth =
|
|
654
|
+
(await deps.auth.authorizeWrite({ request, scope: scopeParam })) ??
|
|
655
|
+
undefined;
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
await deps.auth.authorizeOwner(request);
|
|
659
|
+
}
|
|
660
|
+
// A write that fails before commit hands the proof back (replay guard)
|
|
661
|
+
// so the builder's retry with the same still-valid proof is accepted;
|
|
662
|
+
// after commit the proof stays consumed (a retry would be a duplicate).
|
|
663
|
+
let committed = false;
|
|
664
|
+
const failWrite = async (response) => {
|
|
665
|
+
await writeAuth?.releaseProof?.();
|
|
666
|
+
return response;
|
|
667
|
+
};
|
|
668
|
+
try {
|
|
669
|
+
const scopeResult = parseDataScopeContract(scopeParam);
|
|
670
|
+
if (!scopeResult.ok)
|
|
671
|
+
return failWrite(contractErrorResponse(scopeResult));
|
|
672
|
+
const collectedAtValue = collectedAt(deps.now ?? (() => new Date()));
|
|
673
|
+
const status = deps.syncManager ? "syncing" : "stored";
|
|
674
|
+
// Builder writes land in the same access log as builder reads, so the
|
|
675
|
+
// owner sees who wrote what under which grant.
|
|
676
|
+
// Best-effort: it runs AFTER the record is committed, so a log failure
|
|
677
|
+
// must not turn a successful write into a 500 (the builder would retry
|
|
678
|
+
// and store a duplicate). The ingest log line above still names the
|
|
679
|
+
// builder; the failure itself is logged for the owner.
|
|
680
|
+
const logBuilderWrite = async () => {
|
|
681
|
+
if (!writeAuth)
|
|
682
|
+
return;
|
|
683
|
+
try {
|
|
684
|
+
await deps.accessLogWriter.write({
|
|
685
|
+
logId: deps.createLogId?.() ?? crypto.randomUUID(),
|
|
686
|
+
grantId: writeAuth.grantId,
|
|
687
|
+
builder: writeAuth.builder,
|
|
688
|
+
action: "write",
|
|
689
|
+
scope: scopeResult.scope,
|
|
690
|
+
timestamp: (deps.now ?? (() => new Date()))().toISOString(),
|
|
691
|
+
ipAddress: request.headers.get("x-forwarded-for") ??
|
|
692
|
+
request.headers.get("x-real-ip") ??
|
|
693
|
+
"unknown",
|
|
694
|
+
userAgent: request.headers.get("user-agent") ?? "unknown",
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
deps.logger?.warn?.({
|
|
699
|
+
scope: scopeResult.scope,
|
|
700
|
+
builder: writeAuth.builder,
|
|
701
|
+
grantId: writeAuth.grantId,
|
|
702
|
+
error: err instanceof Error ? err.message : String(err),
|
|
703
|
+
}, "Builder write access-log entry failed; record already stored");
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
// Binary / unstructured data (e.g. a PDF): the body is raw bytes. DPv2
|
|
707
|
+
// data points are scope-addressed and carry no schemaId, so unstructured
|
|
708
|
+
// data needs no schema at all — we ingest it schemaless. (Structured JSON
|
|
709
|
+
// below still resolves a schema for validation/metadata.)
|
|
710
|
+
if (!isJsonContentType(request)) {
|
|
711
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
712
|
+
const metadata = parseMetadataHeader(request.headers.get("x-vana-metadata"));
|
|
713
|
+
// A binary derivative names its sources inside the metadata
|
|
714
|
+
// object; the validated mirror lands at the top of the record.
|
|
715
|
+
const lineage = await prepareWriteLineage(deps, scopeResult.scope, extractLineageField(metadata));
|
|
716
|
+
const result = await ingestBinaryDataContract({
|
|
717
|
+
storage: deps.storage,
|
|
718
|
+
scopeParam: scopeResult.scope,
|
|
719
|
+
bytes,
|
|
720
|
+
mimeType: binaryMimeType(request),
|
|
721
|
+
filename: binaryFilename(request),
|
|
722
|
+
metadata,
|
|
723
|
+
collectedAt: collectedAtValue,
|
|
724
|
+
status,
|
|
725
|
+
attribution: writeAuth?.attribution,
|
|
726
|
+
lineage,
|
|
727
|
+
});
|
|
728
|
+
if (!result.ok)
|
|
729
|
+
return failWrite(contractErrorResponse(result));
|
|
730
|
+
committed = true;
|
|
731
|
+
deps.logger?.info?.({
|
|
732
|
+
scope: scopeResult.scope,
|
|
733
|
+
collectedAt: collectedAtValue,
|
|
734
|
+
path: result.writeResult.relativePath,
|
|
735
|
+
mimeType: binaryMimeType(request),
|
|
736
|
+
sizeBytes: bytes.length,
|
|
737
|
+
...(writeAuth ? { builder: writeAuth.builder } : {}),
|
|
738
|
+
}, "Binary data file ingested");
|
|
739
|
+
await logBuilderWrite();
|
|
740
|
+
notifyNewData(deps.syncManager);
|
|
741
|
+
return jsonResponse(result.response, { status: 201 });
|
|
742
|
+
}
|
|
743
|
+
const parsed = await parseJsonObjectBody(request, "Request body must be valid JSON");
|
|
744
|
+
if (!parsed.ok)
|
|
745
|
+
return failWrite(contractResponse(parsed.result));
|
|
746
|
+
// Derivative writes: validate the caller's `lineage` before anything
|
|
747
|
+
// is stored (a failure here throws, releasing the builder's proof).
|
|
748
|
+
const lineage = await prepareWriteLineage(deps, scopeResult.scope, extractLineageField(parsed.body));
|
|
749
|
+
// DPv2 is scope-addressed and the gateway records no schemas, so JSON
|
|
750
|
+
// ingest is schemaless too — no lookup, no schemaId/$schema stamped.
|
|
751
|
+
const result = await ingestDataContract({
|
|
543
752
|
storage: deps.storage,
|
|
544
753
|
scopeParam: scopeResult.scope,
|
|
545
|
-
|
|
546
|
-
mimeType: binaryMimeType(request),
|
|
547
|
-
filename: binaryFilename(request),
|
|
548
|
-
metadata: parseMetadataHeader(request.headers.get("x-vana-metadata")),
|
|
754
|
+
body: parsed.body,
|
|
549
755
|
collectedAt: collectedAtValue,
|
|
550
756
|
status,
|
|
757
|
+
attribution: writeAuth?.attribution,
|
|
758
|
+
lineage,
|
|
551
759
|
});
|
|
552
760
|
if (!result.ok)
|
|
553
|
-
return contractErrorResponse(result);
|
|
761
|
+
return failWrite(contractErrorResponse(result));
|
|
762
|
+
committed = true;
|
|
554
763
|
deps.logger?.info?.({
|
|
555
764
|
scope: scopeResult.scope,
|
|
556
765
|
collectedAt: collectedAtValue,
|
|
557
766
|
path: result.writeResult.relativePath,
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
767
|
+
...(writeAuth ? { builder: writeAuth.builder } : {}),
|
|
768
|
+
}, "Data file ingested");
|
|
769
|
+
await logBuilderWrite();
|
|
561
770
|
notifyNewData(deps.syncManager);
|
|
562
771
|
return jsonResponse(result.response, { status: 201 });
|
|
563
772
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}, "Data file ingested");
|
|
583
|
-
notifyNewData(deps.syncManager);
|
|
584
|
-
return jsonResponse(result.response, { status: 201 });
|
|
773
|
+
catch (err) {
|
|
774
|
+
// An envelope that reached storage before indexing failed is
|
|
775
|
+
// persisted (a re-index surfaces it): keep the proof consumed so a
|
|
776
|
+
// retry cannot store it twice; the builder needs a fresh proof.
|
|
777
|
+
if (err instanceof IngestPersistedError) {
|
|
778
|
+
deps.logger?.error?.({
|
|
779
|
+
scope: scopeParam,
|
|
780
|
+
path: err.relativePath,
|
|
781
|
+
error: err.cause instanceof Error
|
|
782
|
+
? err.cause.message
|
|
783
|
+
: String(err.cause),
|
|
784
|
+
}, "Envelope written but indexing failed; record persisted unindexed");
|
|
785
|
+
}
|
|
786
|
+
else if (!committed) {
|
|
787
|
+
await writeAuth?.releaseProof?.();
|
|
788
|
+
}
|
|
789
|
+
throw err;
|
|
790
|
+
}
|
|
585
791
|
}
|
|
586
792
|
if (request.method === "DELETE") {
|
|
587
793
|
await deps.auth.authorizeOwner(request);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// still 400s without a
|
|
794
|
+
const cascade = url.searchParams.get("cascade");
|
|
795
|
+
if (cascade !== null && cascade !== "lineage") {
|
|
796
|
+
throw new InvalidCascadeError({ cascade });
|
|
797
|
+
}
|
|
798
|
+
// Parse the scope first so an invalid scope still 400s without a
|
|
799
|
+
// wasted remote call.
|
|
593
800
|
const parsed = parseDataScopeContract(scopeParam);
|
|
594
|
-
if (parsed.ok
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
801
|
+
if (!parsed.ok)
|
|
802
|
+
return contractErrorResponse(parsed);
|
|
803
|
+
if (cascade === "lineage") {
|
|
804
|
+
// Specified (docs/derivative-data-api.md, "Delete") but not
|
|
805
|
+
// implemented here: the cascade must tombstone every derivative at
|
|
806
|
+
// the gateway, and DPv2 deletion is separate work (the tombstone
|
|
807
|
+
// based delete branch). Until that lands the only honest answer is
|
|
808
|
+
// 501; a local-only cascade would report derivatives deleted while
|
|
809
|
+
// their gateway records and ciphertext remain and sync could bring
|
|
810
|
+
// them back.
|
|
811
|
+
throw new LineageCascadeUnavailableError({ scope: parsed.scope });
|
|
601
812
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
deps
|
|
813
|
+
// Single node (the default): propagate the deletion to the
|
|
814
|
+
// authoritative stores (R2 blobs + gateway records) BEFORE the local
|
|
815
|
+
// delete, which reads the local index to find what to remove.
|
|
816
|
+
// Best-effort: a remote failure must not block the owner's local delete
|
|
817
|
+
// (sync would otherwise resurrect it; the download-worker
|
|
818
|
+
// reconciliation is the backstop).
|
|
819
|
+
await deleteOneScope(deps, parsed.scope);
|
|
609
820
|
return new Response(null, { status: 204 });
|
|
610
821
|
}
|
|
611
822
|
return methodNotAllowed();
|