@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.
Files changed (79) hide show
  1. package/dist/api/index.d.ts +41 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +298 -87
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/contracts/binary.d.ts +12 -0
  6. package/dist/contracts/binary.d.ts.map +1 -1
  7. package/dist/contracts/binary.js +31 -0
  8. package/dist/contracts/binary.js.map +1 -1
  9. package/dist/contracts/data.d.ts +35 -0
  10. package/dist/contracts/data.d.ts.map +1 -1
  11. package/dist/contracts/data.js +96 -14
  12. package/dist/contracts/data.js.map +1 -1
  13. package/dist/contracts/index.d.ts +1 -1
  14. package/dist/contracts/index.d.ts.map +1 -1
  15. package/dist/contracts/index.js +1 -1
  16. package/dist/contracts/index.js.map +1 -1
  17. package/dist/errors/catalog.d.ts +34 -0
  18. package/dist/errors/catalog.d.ts.map +1 -1
  19. package/dist/errors/catalog.js +41 -0
  20. package/dist/errors/catalog.js.map +1 -1
  21. package/dist/lineage/attestation.d.ts +42 -0
  22. package/dist/lineage/attestation.d.ts.map +1 -0
  23. package/dist/lineage/attestation.js +25 -0
  24. package/dist/lineage/attestation.js.map +1 -0
  25. package/dist/lineage/gateway.d.ts +105 -0
  26. package/dist/lineage/gateway.d.ts.map +1 -0
  27. package/dist/lineage/gateway.js +242 -0
  28. package/dist/lineage/gateway.js.map +1 -0
  29. package/dist/lineage/index.d.ts +5 -0
  30. package/dist/lineage/index.d.ts.map +1 -0
  31. package/dist/lineage/index.js +5 -0
  32. package/dist/lineage/index.js.map +1 -0
  33. package/dist/lineage/lineage.d.ts +141 -0
  34. package/dist/lineage/lineage.d.ts.map +1 -0
  35. package/dist/lineage/lineage.js +255 -0
  36. package/dist/lineage/lineage.js.map +1 -0
  37. package/dist/logging/access-log.d.ts +1 -1
  38. package/dist/logging/access-log.d.ts.map +1 -1
  39. package/dist/policy/data-read.d.ts +8 -0
  40. package/dist/policy/data-read.d.ts.map +1 -1
  41. package/dist/policy/data-read.js +9 -2
  42. package/dist/policy/data-read.js.map +1 -1
  43. package/dist/policy/data-write.d.ts +69 -0
  44. package/dist/policy/data-write.d.ts.map +1 -0
  45. package/dist/policy/data-write.js +131 -0
  46. package/dist/policy/data-write.js.map +1 -0
  47. package/dist/policy/index.d.ts +2 -1
  48. package/dist/policy/index.d.ts.map +1 -1
  49. package/dist/policy/index.js +2 -1
  50. package/dist/policy/index.js.map +1 -1
  51. package/dist/signing/request-signer.d.ts +2 -0
  52. package/dist/signing/request-signer.d.ts.map +1 -1
  53. package/dist/signing/request-signer.js +1 -0
  54. package/dist/signing/request-signer.js.map +1 -1
  55. package/dist/signing/signer.d.ts +8 -0
  56. package/dist/signing/signer.d.ts.map +1 -1
  57. package/dist/signing/signer.js +9 -0
  58. package/dist/signing/signer.js.map +1 -1
  59. package/dist/sync/data-point-id.d.ts +7 -0
  60. package/dist/sync/data-point-id.d.ts.map +1 -0
  61. package/dist/sync/data-point-id.js +16 -0
  62. package/dist/sync/data-point-id.js.map +1 -0
  63. package/dist/sync/workers/upload.d.ts +10 -6
  64. package/dist/sync/workers/upload.d.ts.map +1 -1
  65. package/dist/sync/workers/upload.js +39 -17
  66. package/dist/sync/workers/upload.js.map +1 -1
  67. package/dist/write/attribution.d.ts +167 -0
  68. package/dist/write/attribution.d.ts.map +1 -0
  69. package/dist/write/attribution.js +325 -0
  70. package/dist/write/attribution.js.map +1 -0
  71. package/dist/write/index.d.ts +3 -0
  72. package/dist/write/index.d.ts.map +1 -0
  73. package/dist/write/index.js +3 -0
  74. package/dist/write/index.js.map +1 -0
  75. package/dist/write/session.d.ts +96 -0
  76. package/dist/write/session.d.ts.map +1 -0
  77. package/dist/write/session.js +136 -0
  78. package/dist/write/session.js.map +1 -0
  79. package/package.json +9 -1
@@ -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">;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
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
- await deps.auth.authorizeOwner(request);
531
- const scopeResult = parseDataScopeContract(scopeParam);
532
- if (!scopeResult.ok)
533
- return contractErrorResponse(scopeResult);
534
- const collectedAtValue = collectedAt(deps.now ?? (() => new Date()));
535
- const status = deps.syncManager ? "syncing" : "stored";
536
- // Binary / unstructured data (e.g. a PDF): the body is raw bytes. DPv2
537
- // data points are scope-addressed and carry no schemaId, so unstructured
538
- // data needs no schema at all — we ingest it schemaless. (Structured JSON
539
- // below still resolves a schema for validation/metadata.)
540
- if (!isJsonContentType(request)) {
541
- const bytes = new Uint8Array(await request.arrayBuffer());
542
- const result = await ingestBinaryDataContract({
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
- bytes,
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
- mimeType: binaryMimeType(request),
559
- sizeBytes: bytes.length,
560
- }, "Binary data file ingested");
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
- const parsed = await parseJsonObjectBody(request, "Request body must be valid JSON");
565
- if (!parsed.ok)
566
- return contractResponse(parsed.result);
567
- // DPv2 is scope-addressed and the gateway records no schemas, so JSON
568
- // ingest is schemaless too — no lookup, no schemaId/$schema stamped.
569
- const result = await ingestDataContract({
570
- storage: deps.storage,
571
- scopeParam: scopeResult.scope,
572
- body: parsed.body,
573
- collectedAt: collectedAtValue,
574
- status,
575
- });
576
- if (!result.ok)
577
- return contractErrorResponse(result);
578
- deps.logger?.info?.({
579
- scope: scopeResult.scope,
580
- collectedAt: collectedAtValue,
581
- path: result.writeResult.relativePath,
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
- // Propagate the deletion to the authoritative stores (R2 blobs + gateway records) BEFORE the
589
- // local delete it reads the local index to find what to remove. Best-effort: a remote
590
- // failure must not block the owner's local delete (and sync would otherwise resurrect it; the
591
- // download-worker reconciliation is the backstop). Parse the scope first so an invalid scope
592
- // still 400s without a wasted remote call.
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 && deps.syncManager?.deleteScopeRemote) {
595
- try {
596
- await deps.syncManager.deleteScopeRemote(parsed.scope);
597
- }
598
- catch (err) {
599
- deps.logger?.info?.({ scope: scopeParam, error: err.message }, "Remote scope deletion failed; proceeding with local delete");
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
- const result = await deleteDataScopeContract({
603
- storage: deps.storage,
604
- scopeParam,
605
- });
606
- if (!result.ok)
607
- return contractErrorResponse(result);
608
- deps.logger?.info?.({ scope: scopeParam, deletedCount: result.deletedCount }, "Scope deleted");
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();