@opendatalabs/personal-server-ts-core 1.6.0 → 1.8.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 +53 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +300 -25
- package/dist/api/index.js.map +1 -1
- package/dist/contracts/data.d.ts +30 -1
- package/dist/contracts/data.d.ts.map +1 -1
- package/dist/contracts/data.js +126 -33
- package/dist/contracts/data.js.map +1 -1
- package/dist/errors/catalog.d.ts +40 -0
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/catalog.js +53 -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 +7 -1
- package/dist/logging/access-log.d.ts.map +1 -1
- package/dist/mcp/read-client.d.ts.map +1 -1
- package/dist/mcp/read-client.js +25 -1
- package/dist/mcp/read-client.js.map +1 -1
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +10 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/ports/index.d.ts +125 -0
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/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/storage/adapters/index.d.ts +1 -1
- package/dist/storage/adapters/index.d.ts.map +1 -1
- package/dist/storage/adapters/index.js +1 -1
- package/dist/storage/adapters/index.js.map +1 -1
- package/dist/storage/adapters/vana.d.ts +2 -0
- package/dist/storage/adapters/vana.d.ts.map +1 -1
- package/dist/storage/adapters/vana.js +5 -1
- package/dist/storage/adapters/vana.js.map +1 -1
- package/dist/storage/index/types.d.ts +1 -0
- package/dist/storage/index/types.d.ts.map +1 -1
- package/dist/sync/data-point-feed.d.ts +35 -0
- package/dist/sync/data-point-feed.d.ts.map +1 -0
- package/dist/sync/data-point-feed.js +122 -0
- package/dist/sync/data-point-feed.js.map +1 -0
- 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/delete-data-port.d.ts +50 -0
- package/dist/sync/delete-data-port.d.ts.map +1 -0
- package/dist/sync/delete-data-port.js +244 -0
- package/dist/sync/delete-data-port.js.map +1 -0
- package/dist/sync/engine/sync-manager.d.ts +14 -4
- package/dist/sync/engine/sync-manager.d.ts.map +1 -1
- package/dist/sync/engine/sync-manager.js +53 -8
- package/dist/sync/engine/sync-manager.js.map +1 -1
- package/dist/sync/index.d.ts +7 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +7 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/pending-blob-deletions.d.ts +29 -0
- package/dist/sync/pending-blob-deletions.d.ts.map +1 -0
- package/dist/sync/pending-blob-deletions.js +116 -0
- package/dist/sync/pending-blob-deletions.js.map +1 -0
- package/dist/sync/scope-deletions.d.ts +155 -0
- package/dist/sync/scope-deletions.d.ts.map +1 -0
- package/dist/sync/scope-deletions.js +225 -0
- package/dist/sync/scope-deletions.js.map +1 -0
- package/dist/sync/tombstone.d.ts +27 -0
- package/dist/sync/tombstone.d.ts.map +1 -0
- package/dist/sync/tombstone.js +28 -0
- package/dist/sync/tombstone.js.map +1 -0
- package/dist/sync/workers/delete.d.ts +152 -31
- package/dist/sync/workers/delete.d.ts.map +1 -1
- package/dist/sync/workers/delete.js +461 -27
- package/dist/sync/workers/delete.js.map +1 -1
- package/dist/sync/workers/download.d.ts +38 -1
- package/dist/sync/workers/download.d.ts.map +1 -1
- package/dist/sync/workers/download.js +106 -2
- package/dist/sync/workers/download.js.map +1 -1
- package/dist/sync/workers/upload.d.ts +41 -7
- package/dist/sync/workers/upload.d.ts.map +1 -1
- package/dist/sync/workers/upload.js +147 -21
- package/dist/sync/workers/upload.js.map +1 -1
- package/dist/write/attribution.d.ts +6 -4
- package/dist/write/attribution.d.ts.map +1 -1
- package/dist/write/attribution.js +51 -3
- package/dist/write/attribution.js.map +1 -1
- package/package.json +5 -1
package/dist/api/index.d.ts
CHANGED
|
@@ -3,10 +3,14 @@ import type { AccessLogReader } from "../logging/access-reader.js";
|
|
|
3
3
|
import { type DataStoragePort, type RuntimeAvailabilityPort } from "../ports/index.js";
|
|
4
4
|
import type { DataReadPolicyPorts } from "../policy/index.js";
|
|
5
5
|
import type { SyncManager } from "../sync/index.js";
|
|
6
|
+
import { type ScopeDeletionTracker } from "../sync/scope-deletions.js";
|
|
7
|
+
import type { IndexEntry } from "../storage/index/types.js";
|
|
8
|
+
import { type DeleteScopeResult } from "../sync/workers/delete.js";
|
|
6
9
|
import { type OAuthDeviceSessionLookup, type OAuthTokenStorePort } from "../contracts/index.js";
|
|
7
10
|
import type { DataPortabilityGatewayConfig, GatewayClient } from "@opendatalabs/vana-sdk/browser";
|
|
8
11
|
import type { ServerSigner } from "../signing/index.js";
|
|
9
12
|
import type { WriterAttribution } from "../write/attribution.js";
|
|
13
|
+
import type { LineageGatewayPort } from "../lineage/gateway.js";
|
|
10
14
|
import { type X402Challenge } from "../payment/index.js";
|
|
11
15
|
export interface PersonalServerReadAuthInput {
|
|
12
16
|
request: Request;
|
|
@@ -83,8 +87,12 @@ export interface PersonalServerApiLogger {
|
|
|
83
87
|
export interface PersonalServerIngestSyncManager {
|
|
84
88
|
trigger(): Promise<void>;
|
|
85
89
|
notifyNewData?(): void;
|
|
86
|
-
/**
|
|
87
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Durable scope deletion (gateway tombstone -> storage blobs -> local).
|
|
92
|
+
* When absent the DELETE route deletes the local copy only and reports
|
|
93
|
+
* `durable: false`.
|
|
94
|
+
*/
|
|
95
|
+
deleteScope?(scope: string): Promise<DeleteScopeResult>;
|
|
88
96
|
}
|
|
89
97
|
export interface PersonalServerDataApiDeps {
|
|
90
98
|
storage: DataStoragePort;
|
|
@@ -93,6 +101,15 @@ export interface PersonalServerDataApiDeps {
|
|
|
93
101
|
syncManager?: PersonalServerIngestSyncManager | null;
|
|
94
102
|
runtimeAvailability?: RuntimeAvailabilityPort;
|
|
95
103
|
readFulfillmentReporter?: PersonalServerReadFulfillmentReporter;
|
|
104
|
+
/**
|
|
105
|
+
* Read-side memory of gateway tombstones (see sync/scope-deletions.ts for
|
|
106
|
+
* the consistency window). Every read consults it before serving or
|
|
107
|
+
* charging: a scope the gateway reports deleted answers 410 DATA_DELETED
|
|
108
|
+
* whether or not a stale local copy still exists, and a local miss on a
|
|
109
|
+
* deleted scope answers 410 instead of 404 so SDK readers can tell
|
|
110
|
+
* "deleted" from "never had it". Without it reads are local-only.
|
|
111
|
+
*/
|
|
112
|
+
scopeDeletions?: ScopeDeletionTracker;
|
|
96
113
|
/**
|
|
97
114
|
* Required when payment is on. Powers two things on GET /v1/data/:scope:
|
|
98
115
|
* - the X402 challenge generation (fee lookup, accessRecord binding)
|
|
@@ -155,6 +172,13 @@ export interface PersonalServerDataApiDeps {
|
|
|
155
172
|
now?: () => Date;
|
|
156
173
|
createLogId?: () => string;
|
|
157
174
|
logger?: PersonalServerApiLogger;
|
|
175
|
+
/**
|
|
176
|
+
* Gateway access for derivative data (docs/derivative-data-api.md): source
|
|
177
|
+
* lookups for lineage validation on write and the signed lineage read
|
|
178
|
+
* behind GET /v1/data/:scope/lineage. Absent = writes with lineage can
|
|
179
|
+
* only cite local scopes and the lineage read answers 503.
|
|
180
|
+
*/
|
|
181
|
+
lineageGateway?: LineageGatewayPort;
|
|
158
182
|
}
|
|
159
183
|
export interface PersonalServerAccessLogsApiDeps {
|
|
160
184
|
auth: Pick<PersonalServerApiAuthPort, "authorizeOwner">;
|
|
@@ -229,6 +253,33 @@ export type X402CycleResult = {
|
|
|
229
253
|
* 409 (replay) from 400 (amount mismatch).
|
|
230
254
|
*/
|
|
231
255
|
export declare function handleX402Cycle(input: X402CycleInput): Promise<X402CycleResult>;
|
|
256
|
+
/**
|
|
257
|
+
* The deletion that applies to a read of `scope`, or null when the read may
|
|
258
|
+
* be served. `entry` is the local version the read would serve (undefined
|
|
259
|
+
* on a local miss).
|
|
260
|
+
*
|
|
261
|
+
* - A local copy is refused when the gateway holds a tombstone that covers
|
|
262
|
+
* it (see `isEntryCoveredByTombstone`: registry versions and the ingest
|
|
263
|
+
* marker, never clocks). A local re-add on top of the tombstone is served.
|
|
264
|
+
* - A local miss asks the tracker to consult the gateway (bounded by its
|
|
265
|
+
* per-scope verdict cache and failure back-off): the local index cannot
|
|
266
|
+
* tell "deleted long ago" from "never had it".
|
|
267
|
+
* Gateway unreachability never fails the read: the tracker answers from its
|
|
268
|
+
* last known state, which is the documented offline behaviour.
|
|
269
|
+
*/
|
|
270
|
+
export declare function resolveReadDeletion(deps: Pick<PersonalServerDataApiDeps, "scopeDeletions" | "serverOwner">, scope: string, entry: ReadDeletionEntry | null | undefined): Promise<{
|
|
271
|
+
scope: string;
|
|
272
|
+
dataPointId: string | null;
|
|
273
|
+
deletedAt: string;
|
|
274
|
+
} | null>;
|
|
275
|
+
/**
|
|
276
|
+
* Throw 410 DATA_DELETED when `resolveReadDeletion` says the read must not be
|
|
277
|
+
* served. Call it before any payment step: a builder must never be charged
|
|
278
|
+
* for a scope the owner deleted.
|
|
279
|
+
*/
|
|
280
|
+
export declare function assertScopeNotDeleted(deps: Pick<PersonalServerDataApiDeps, "scopeDeletions" | "serverOwner">, scope: string, entry: ReadDeletionEntry | null | undefined): Promise<void>;
|
|
281
|
+
/** The index fields the deletion gate needs from the entry a read would serve. */
|
|
282
|
+
export type ReadDeletionEntry = Pick<IndexEntry, "version" | "dataPointId" | "afterTombstoneVersion">;
|
|
232
283
|
export declare function reportPersonalServerReadFulfillment(deps: PersonalServerDataApiDeps, event: PersonalServerReadFulfillment): void;
|
|
233
284
|
export declare function handlePersonalServerDataRequest(request: Request, deps: PersonalServerDataApiDeps, options?: PersonalServerApiDispatchOptions): Promise<Response>;
|
|
234
285
|
export declare function handlePersonalServerAccessLogsRequest(request: Request, deps: PersonalServerAccessLogsApiDeps, options?: PersonalServerApiDispatchOptions): Promise<Response>;
|
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,9 @@
|
|
|
1
|
-
import { ProtocolError } from "../errors/catalog.js";
|
|
2
|
-
import {
|
|
1
|
+
import { DataDeletedError, DeleteTombstoneFailedError, GrantRequiredError, InvalidCascadeError, LineageCascadeUnavailableError, LineageGatewayError, LineageUnavailableError, ProtocolError, ServerNotConfiguredError, } from "../errors/catalog.js";
|
|
2
|
+
import { computeDataPointId } from "../sync/data-point-id.js";
|
|
3
|
+
import { isEntryCoveredByTombstone, } from "../sync/scope-deletions.js";
|
|
4
|
+
import { deleteScope as deleteScopeLocally, } from "../sync/workers/delete.js";
|
|
5
|
+
import { 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";
|
|
6
|
+
import { extractLineageField, prepareLineage, } from "../lineage/lineage.js";
|
|
3
7
|
import { binaryFilename, binaryMimeType, isJsonContentType, } from "../contracts/binary.js";
|
|
4
8
|
import { buildChallenge, parsePaymentHeader, verifyPayment, } from "../payment/index.js";
|
|
5
9
|
function jsonResponse(body, init) {
|
|
@@ -250,6 +254,86 @@ function collectedAt(now) {
|
|
|
250
254
|
.toISOString()
|
|
251
255
|
.replace(/\.\d{3}Z$/, "Z");
|
|
252
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* The deletion that applies to a read of `scope`, or null when the read may
|
|
259
|
+
* be served. `entry` is the local version the read would serve (undefined
|
|
260
|
+
* on a local miss).
|
|
261
|
+
*
|
|
262
|
+
* - A local copy is refused when the gateway holds a tombstone that covers
|
|
263
|
+
* it (see `isEntryCoveredByTombstone`: registry versions and the ingest
|
|
264
|
+
* marker, never clocks). A local re-add on top of the tombstone is served.
|
|
265
|
+
* - A local miss asks the tracker to consult the gateway (bounded by its
|
|
266
|
+
* per-scope verdict cache and failure back-off): the local index cannot
|
|
267
|
+
* tell "deleted long ago" from "never had it".
|
|
268
|
+
* Gateway unreachability never fails the read: the tracker answers from its
|
|
269
|
+
* last known state, which is the documented offline behaviour.
|
|
270
|
+
*/
|
|
271
|
+
export async function resolveReadDeletion(deps, scope, entry) {
|
|
272
|
+
if (!deps.scopeDeletions)
|
|
273
|
+
return null;
|
|
274
|
+
const verdict = await deps.scopeDeletions.resolve(scope, {
|
|
275
|
+
consultGateway: entry ? "if-stale" : "always",
|
|
276
|
+
});
|
|
277
|
+
if (!verdict.deleted)
|
|
278
|
+
return null;
|
|
279
|
+
if (entry && !isEntryCoveredByTombstone(entry, verdict)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
scope,
|
|
284
|
+
dataPointId: deps.serverOwner
|
|
285
|
+
? computeDataPointId(deps.serverOwner, scope)
|
|
286
|
+
: null,
|
|
287
|
+
deletedAt: verdict.deletedAt,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Throw 410 DATA_DELETED when `resolveReadDeletion` says the read must not be
|
|
292
|
+
* served. Call it before any payment step: a builder must never be charged
|
|
293
|
+
* for a scope the owner deleted.
|
|
294
|
+
*/
|
|
295
|
+
export async function assertScopeNotDeleted(deps, scope, entry) {
|
|
296
|
+
const deletion = await resolveReadDeletion(deps, scope, entry);
|
|
297
|
+
if (deletion)
|
|
298
|
+
throw new DataDeletedError(deletion);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Discovery filter for the scope and version listings: an entry a read would
|
|
302
|
+
* refuse with 410 is not listed either. Undefined (no filtering) when no
|
|
303
|
+
* deletion tracker is wired.
|
|
304
|
+
*/
|
|
305
|
+
function discoveryVisibility(deps) {
|
|
306
|
+
if (!deps.scopeDeletions)
|
|
307
|
+
return undefined;
|
|
308
|
+
return async (scope, entry) => (await resolveReadDeletion(deps, scope, entry)) === null;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The causal marker a fresh ingest carries: the tombstone version this
|
|
312
|
+
* replica currently knows for the scope, if any, so the new entry is a
|
|
313
|
+
* deliberate re-add that survives that deletion (and only that one). Uses
|
|
314
|
+
* the same verdict path as reads, so it costs a gateway lookup only when the
|
|
315
|
+
* tracker's view is stale. An unknown or unsafe version leaves the marker
|
|
316
|
+
* null: the entry is then covered, never resurrected on a guess.
|
|
317
|
+
*/
|
|
318
|
+
async function ingestTombstoneMarker(deps, scope) {
|
|
319
|
+
if (!deps.scopeDeletions)
|
|
320
|
+
return null;
|
|
321
|
+
const verdict = await deps.scopeDeletions.resolve(scope);
|
|
322
|
+
if (!verdict.deleted || verdict.version === null)
|
|
323
|
+
return null;
|
|
324
|
+
const version = Number(verdict.version);
|
|
325
|
+
return Number.isSafeInteger(version) ? version : null;
|
|
326
|
+
}
|
|
327
|
+
// The delete worker wants a full Logger; the API logger is all-optional.
|
|
328
|
+
function apiLoggerAsLogger(logger) {
|
|
329
|
+
const noop = () => undefined;
|
|
330
|
+
return {
|
|
331
|
+
debug: (payload, message) => (logger?.debug ?? noop)(payload, message ?? ""),
|
|
332
|
+
info: (payload, message) => (logger?.info ?? noop)(payload, message ?? ""),
|
|
333
|
+
warn: (payload, message) => (logger?.warn ?? noop)(payload, message ?? ""),
|
|
334
|
+
error: (payload, message) => (logger?.error ?? noop)(payload, message ?? ""),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
253
337
|
function notifyNewData(syncManager) {
|
|
254
338
|
if (!syncManager)
|
|
255
339
|
return;
|
|
@@ -283,6 +367,47 @@ export function reportPersonalServerReadFulfillment(deps, event) {
|
|
|
283
367
|
warnReadFulfillmentReporterFailed(deps, event, err);
|
|
284
368
|
}
|
|
285
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Validate a write's caller-supplied `lineage` (JSON body top level, or the
|
|
372
|
+
* binary metadata object) into the `$lineage` record to stamp. `undefined`
|
|
373
|
+
* (or null) means a root record. Throws ProtocolErrors; the write handler
|
|
374
|
+
* lets them propagate before anything is stored.
|
|
375
|
+
*/
|
|
376
|
+
async function prepareWriteLineage(deps, scope, field) {
|
|
377
|
+
if (field === undefined || field === null)
|
|
378
|
+
return undefined;
|
|
379
|
+
return prepareLineage({
|
|
380
|
+
scope,
|
|
381
|
+
field,
|
|
382
|
+
serverOwner: deps.serverOwner,
|
|
383
|
+
storage: deps.storage,
|
|
384
|
+
gateway: deps.lineageGateway,
|
|
385
|
+
now: deps.now ?? (() => new Date()),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The lineage view a read auth result entitles the caller to. The auth port
|
|
390
|
+
* answers an owner read with no result or with the "owner" / "policy-bypass"
|
|
391
|
+
* sentinels (see api-auth): those get the full view. A builder read carries
|
|
392
|
+
* the grant the read policy resolved, and that grant names the view. A
|
|
393
|
+
* builder result with no resolved grant (or an "unknown" one) proves
|
|
394
|
+
* nothing and is refused: a missing grantId must never widen to the full
|
|
395
|
+
* view.
|
|
396
|
+
*/
|
|
397
|
+
function resolveLineageGrantView(authResult) {
|
|
398
|
+
if (authResult === undefined ||
|
|
399
|
+
authResult.grantId === "owner" ||
|
|
400
|
+
authResult.grantId === "policy-bypass") {
|
|
401
|
+
return { grantId: undefined };
|
|
402
|
+
}
|
|
403
|
+
const grantId = authResult.grantId;
|
|
404
|
+
if (typeof grantId !== "string" || grantId === "" || grantId === "unknown") {
|
|
405
|
+
throw new GrantRequiredError({
|
|
406
|
+
reason: "a lineage read by a builder needs a resolved grant",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return { grantId };
|
|
410
|
+
}
|
|
286
411
|
export async function handlePersonalServerDataRequest(request, deps, options = {}) {
|
|
287
412
|
return withApiErrors(async () => {
|
|
288
413
|
const url = new URL(request.url);
|
|
@@ -296,6 +421,7 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
296
421
|
scopePrefix: url.searchParams.get("scopePrefix") ?? undefined,
|
|
297
422
|
limit: normalizeLimit(url.searchParams.get("limit"), 20),
|
|
298
423
|
offset: normalizeLimit(url.searchParams.get("offset"), 0),
|
|
424
|
+
isVisible: discoveryVisibility(deps),
|
|
299
425
|
});
|
|
300
426
|
return jsonResponse(result.response);
|
|
301
427
|
}
|
|
@@ -304,16 +430,93 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
304
430
|
if (request.method !== "GET")
|
|
305
431
|
return methodNotAllowed();
|
|
306
432
|
await deps.auth.authorizeBuilderList(request);
|
|
307
|
-
const result = listDataVersionsContract({
|
|
433
|
+
const result = await listDataVersionsContract({
|
|
308
434
|
storage: deps.storage,
|
|
309
435
|
scopeParam: decodePathPart(parts[0]),
|
|
310
436
|
limit: normalizeLimit(url.searchParams.get("limit"), 20),
|
|
311
437
|
offset: normalizeLimit(url.searchParams.get("offset"), 0),
|
|
438
|
+
isVisible: discoveryVisibility(deps),
|
|
312
439
|
});
|
|
313
440
|
if (!result.ok)
|
|
314
441
|
return contractErrorResponse(result);
|
|
442
|
+
if (result.response.total === 0) {
|
|
443
|
+
// Nothing visible: answer 410 rather than an empty list when that is
|
|
444
|
+
// because the scope is tombstoned, exactly as a read would.
|
|
445
|
+
await assertScopeNotDeleted(deps, result.scope, undefined);
|
|
446
|
+
}
|
|
315
447
|
return jsonResponse(result.response);
|
|
316
448
|
}
|
|
449
|
+
if ((parts.length === 2 || parts.length === 3) && parts[1] === "lineage") {
|
|
450
|
+
if (request.method !== "GET")
|
|
451
|
+
return methodNotAllowed();
|
|
452
|
+
const scopeResult = parseDataScopeContract(decodePathPart(parts[0]));
|
|
453
|
+
if (!scopeResult.ok)
|
|
454
|
+
return contractErrorResponse(scopeResult);
|
|
455
|
+
// The version is a PATH segment (/v1/data/:scope/lineage/:version), so
|
|
456
|
+
// it is inside the signed request uri: a builder's signature for one
|
|
457
|
+
// version cannot be replayed to read another. The grant view is the
|
|
458
|
+
// signed grantId claim the read auth already honours. Query
|
|
459
|
+
// parameters would be unsigned inputs to the view and are refused.
|
|
460
|
+
if (url.searchParams.has("version")) {
|
|
461
|
+
return errorResponse(400, "INVALID_VERSION", "version is a path segment (/v1/data/:scope/lineage/:version), not a query parameter");
|
|
462
|
+
}
|
|
463
|
+
if (url.search.length > 0) {
|
|
464
|
+
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");
|
|
465
|
+
}
|
|
466
|
+
const version = parts.length === 3 ? decodePathPart(parts[2]) : undefined;
|
|
467
|
+
if (version !== undefined && !/^[1-9]\d*$/.test(version)) {
|
|
468
|
+
return errorResponse(400, "INVALID_VERSION", "version must be a positive decimal integer");
|
|
469
|
+
}
|
|
470
|
+
// Same gate as a read of the scope: the owner, or a builder whose grant
|
|
471
|
+
// covers it. The gateway then attests the view that grant sees, so a
|
|
472
|
+
// builder only learns the scopes of nodes its grant covers.
|
|
473
|
+
const authResult = await deps.auth.authorizeBuilderRead({
|
|
474
|
+
request,
|
|
475
|
+
scope: scopeResult.scope,
|
|
476
|
+
grantId: selectedGrantId(request, url),
|
|
477
|
+
});
|
|
478
|
+
if (!deps.serverOwner) {
|
|
479
|
+
throw new ServerNotConfiguredError({
|
|
480
|
+
reason: "serverOwner is required to resolve the data point id",
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (!deps.lineageGateway)
|
|
484
|
+
throw new LineageUnavailableError();
|
|
485
|
+
const { grantId } = resolveLineageGrantView(authResult);
|
|
486
|
+
let result;
|
|
487
|
+
try {
|
|
488
|
+
result = await deps.lineageGateway.getLineage({
|
|
489
|
+
dataPointId: computeDataPointId(deps.serverOwner, scopeResult.scope),
|
|
490
|
+
version,
|
|
491
|
+
grantId,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
// Transport failures (DNS, refused, timeout) are gateway errors to
|
|
496
|
+
// the caller, not internal ones: same 502 as a gateway error body.
|
|
497
|
+
if (err instanceof ProtocolError)
|
|
498
|
+
throw err;
|
|
499
|
+
throw new LineageGatewayError({
|
|
500
|
+
status: 0,
|
|
501
|
+
body: { error: err instanceof Error ? err.message : String(err) },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (!result.ok) {
|
|
505
|
+
if (result.status === 404) {
|
|
506
|
+
return errorResponse(404, "NOT_FOUND", version
|
|
507
|
+
? `Scope "${scopeResult.scope}" has no registered version ${version}`
|
|
508
|
+
: `Scope "${scopeResult.scope}" is not registered at the gateway`);
|
|
509
|
+
}
|
|
510
|
+
if (result.status === 403) {
|
|
511
|
+
throw new ProtocolError(403, "LINEAGE_FORBIDDEN", "The gateway refused the lineage view for this grant", { gateway: result.body });
|
|
512
|
+
}
|
|
513
|
+
throw new LineageGatewayError({
|
|
514
|
+
status: result.status,
|
|
515
|
+
body: result.body,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return jsonResponse({ data: result.data, proof: result.proof });
|
|
519
|
+
}
|
|
317
520
|
if (parts.length !== 1)
|
|
318
521
|
return notFound();
|
|
319
522
|
const scopeParam = decodePathPart(parts[0]);
|
|
@@ -337,6 +540,10 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
337
540
|
fileId: url.searchParams.get("fileId") ?? selectedEntry?.fileId ?? undefined,
|
|
338
541
|
at: url.searchParams.get("at") ?? undefined,
|
|
339
542
|
});
|
|
543
|
+
// Deletion gate, after auth (so unauthenticated callers learn nothing)
|
|
544
|
+
// and before payment (so nobody is charged for deleted data). Covers
|
|
545
|
+
// both a stale local copy of a tombstoned scope and a local miss.
|
|
546
|
+
await assertScopeNotDeleted(deps, scopeResult.scope, selectedEntry);
|
|
340
547
|
// X402 payment dance for builder reads. Owner-exempt reads (the
|
|
341
548
|
// grantId sentinels "owner" / "policy-bypass") skip payment entirely
|
|
342
549
|
// since there's no payable op to attach the payment to.
|
|
@@ -432,8 +639,15 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
432
639
|
fileId: url.searchParams.get("fileId") ?? undefined,
|
|
433
640
|
at: url.searchParams.get("at") ?? undefined,
|
|
434
641
|
});
|
|
435
|
-
if (!result.ok)
|
|
642
|
+
if (!result.ok) {
|
|
643
|
+
if (result.status === 404) {
|
|
644
|
+
// The entry selected above can vanish before the read (a sync
|
|
645
|
+
// reconcile applying a tombstone concurrently); answer 410, not
|
|
646
|
+
// 404, when that is why.
|
|
647
|
+
await assertScopeNotDeleted(deps, scopeResult.scope, undefined);
|
|
648
|
+
}
|
|
436
649
|
return contractErrorResponse(result);
|
|
650
|
+
}
|
|
437
651
|
const logId = deps.createLogId?.() ?? crypto.randomUUID();
|
|
438
652
|
const timestamp = (deps.now ?? (() => new Date()))().toISOString();
|
|
439
653
|
const ipAddress = request.headers.get("x-forwarded-for") ??
|
|
@@ -531,6 +745,7 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
531
745
|
return failWrite(contractErrorResponse(scopeResult));
|
|
532
746
|
const collectedAtValue = collectedAt(deps.now ?? (() => new Date()));
|
|
533
747
|
const status = deps.syncManager ? "syncing" : "stored";
|
|
748
|
+
const afterTombstoneVersion = await ingestTombstoneMarker(deps, scopeResult.scope);
|
|
534
749
|
// Builder writes land in the same access log as builder reads, so the
|
|
535
750
|
// owner sees who wrote what under which grant.
|
|
536
751
|
// Best-effort: it runs AFTER the record is committed, so a log failure
|
|
@@ -569,16 +784,22 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
569
784
|
// below still resolves a schema for validation/metadata.)
|
|
570
785
|
if (!isJsonContentType(request)) {
|
|
571
786
|
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
787
|
+
const metadata = parseMetadataHeader(request.headers.get("x-vana-metadata"));
|
|
788
|
+
// A binary derivative names its sources inside the metadata
|
|
789
|
+
// object; the validated mirror lands at the top of the record.
|
|
790
|
+
const lineage = await prepareWriteLineage(deps, scopeResult.scope, extractLineageField(metadata));
|
|
572
791
|
const result = await ingestBinaryDataContract({
|
|
573
792
|
storage: deps.storage,
|
|
574
793
|
scopeParam: scopeResult.scope,
|
|
575
794
|
bytes,
|
|
576
795
|
mimeType: binaryMimeType(request),
|
|
577
796
|
filename: binaryFilename(request),
|
|
578
|
-
metadata
|
|
797
|
+
metadata,
|
|
579
798
|
collectedAt: collectedAtValue,
|
|
580
799
|
status,
|
|
581
800
|
attribution: writeAuth?.attribution,
|
|
801
|
+
lineage,
|
|
802
|
+
afterTombstoneVersion,
|
|
582
803
|
});
|
|
583
804
|
if (!result.ok)
|
|
584
805
|
return failWrite(contractErrorResponse(result));
|
|
@@ -598,6 +819,9 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
598
819
|
const parsed = await parseJsonObjectBody(request, "Request body must be valid JSON");
|
|
599
820
|
if (!parsed.ok)
|
|
600
821
|
return failWrite(contractResponse(parsed.result));
|
|
822
|
+
// Derivative writes: validate the caller's `lineage` before anything
|
|
823
|
+
// is stored (a failure here throws, releasing the builder's proof).
|
|
824
|
+
const lineage = await prepareWriteLineage(deps, scopeResult.scope, extractLineageField(parsed.body));
|
|
601
825
|
// DPv2 is scope-addressed and the gateway records no schemas, so JSON
|
|
602
826
|
// ingest is schemaless too — no lookup, no schemaId/$schema stamped.
|
|
603
827
|
const result = await ingestDataContract({
|
|
@@ -607,6 +831,8 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
607
831
|
collectedAt: collectedAtValue,
|
|
608
832
|
status,
|
|
609
833
|
attribution: writeAuth?.attribution,
|
|
834
|
+
lineage,
|
|
835
|
+
afterTombstoneVersion,
|
|
610
836
|
});
|
|
611
837
|
if (!result.ok)
|
|
612
838
|
return failWrite(contractErrorResponse(result));
|
|
@@ -642,28 +868,77 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
|
|
|
642
868
|
}
|
|
643
869
|
if (request.method === "DELETE") {
|
|
644
870
|
await deps.auth.authorizeOwner(request);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
// still 400s without a
|
|
871
|
+
const cascade = url.searchParams.get("cascade");
|
|
872
|
+
if (cascade !== null && cascade !== "lineage") {
|
|
873
|
+
throw new InvalidCascadeError({ cascade });
|
|
874
|
+
}
|
|
875
|
+
// Parse the scope first so an invalid scope still 400s without a
|
|
876
|
+
// wasted remote call.
|
|
650
877
|
const parsed = parseDataScopeContract(scopeParam);
|
|
651
|
-
if (parsed.ok
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
878
|
+
if (!parsed.ok)
|
|
879
|
+
return contractErrorResponse(parsed);
|
|
880
|
+
if (cascade === "lineage") {
|
|
881
|
+
// Specified (docs/derivative-data-api.md, "Delete") but not
|
|
882
|
+
// implemented here. The durable per-scope delete below is what a
|
|
883
|
+
// cascade node needs; the missing part is the lineage walk itself
|
|
884
|
+
// (owner view of the gateway graph, deepest derivatives first,
|
|
885
|
+
// truncation and cross-owner handling, all-or-nothing before the
|
|
886
|
+
// first tombstone). Until that lands the only honest answer is 501.
|
|
887
|
+
throw new LineageCascadeUnavailableError({ scope: parsed.scope });
|
|
658
888
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
889
|
+
// Durable order: gateway tombstone -> storage blobs -> local copy. The
|
|
890
|
+
// sync manager owns the remote ports; without one (sync disabled or
|
|
891
|
+
// no owner key material) only the local copy goes and the result says
|
|
892
|
+
// so (`durable: false`) rather than pretending.
|
|
893
|
+
const result = deps.syncManager?.deleteScope
|
|
894
|
+
? await deps.syncManager.deleteScope(parsed.scope)
|
|
895
|
+
: await deleteScopeLocally({
|
|
896
|
+
storage: deps.storage,
|
|
897
|
+
serverOwner: deps.serverOwner,
|
|
898
|
+
deleteData: null,
|
|
899
|
+
logger: apiLoggerAsLogger(deps.logger),
|
|
900
|
+
}, parsed.scope);
|
|
901
|
+
if (result.steps.gateway.status === "failed") {
|
|
902
|
+
// Nothing was deleted: the local copy is kept on purpose so sync
|
|
903
|
+
// cannot resurrect a half-deleted scope. Surface it as an upstream
|
|
904
|
+
// failure with the per-step result attached.
|
|
905
|
+
throw new DeleteTombstoneFailedError({
|
|
906
|
+
scope: parsed.scope,
|
|
907
|
+
result,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
// Audit entry, best-effort: the deletion above is irreversible and
|
|
911
|
+
// already done, so a failed log write must not turn it into an error
|
|
912
|
+
// response that invites a misleading retry. The failure is logged.
|
|
913
|
+
try {
|
|
914
|
+
await deps.accessLogWriter.write({
|
|
915
|
+
logId: deps.createLogId?.() ?? crypto.randomUUID(),
|
|
916
|
+
grantId: "owner",
|
|
917
|
+
builder: deps.serverOwner ?? "owner",
|
|
918
|
+
action: "delete",
|
|
919
|
+
scope: parsed.scope,
|
|
920
|
+
timestamp: (deps.now ?? (() => new Date()))().toISOString(),
|
|
921
|
+
ipAddress: request.headers.get("x-forwarded-for") ??
|
|
922
|
+
request.headers.get("x-real-ip") ??
|
|
923
|
+
"unknown",
|
|
924
|
+
userAgent: request.headers.get("user-agent") ?? "unknown",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
catch (err) {
|
|
928
|
+
deps.logger?.warn?.({
|
|
929
|
+
scope: parsed.scope,
|
|
930
|
+
error: err instanceof Error ? err.message : String(err),
|
|
931
|
+
}, "Delete access-log entry failed; scope already deleted");
|
|
932
|
+
}
|
|
933
|
+
deps.logger?.info?.({
|
|
934
|
+
scope: parsed.scope,
|
|
935
|
+
durable: result.durable,
|
|
936
|
+
gateway: result.steps.gateway.status,
|
|
937
|
+
storage: result.steps.storage.status,
|
|
938
|
+
local: result.steps.local.status,
|
|
939
|
+
deletedCount: result.steps.local.deletedCount,
|
|
940
|
+
}, "Scope deleted");
|
|
941
|
+
return jsonResponse(result, { status: 200 });
|
|
667
942
|
}
|
|
668
943
|
return methodNotAllowed();
|
|
669
944
|
});
|