@ariestools/cli 0.1.11 → 0.1.13

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.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
- import * as nc from "node:crypto";
5
- import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
6
4
  import http from "http";
7
5
  import https from "https";
8
6
  import { gunzipSync } from "zlib";
9
- import { createHash as createHash$3, createHmac as createHmac$1, pbkdf2Sync, randomBytes as randomBytes$6 } from "crypto";
7
+ import { createHash as createHash$2, createHmac as createHmac$1, pbkdf2Sync, randomBytes as randomBytes$6 } from "crypto";
8
+ import * as nc from "node:crypto";
9
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
10
10
  import os, { cpus } from "node:os";
11
11
  import process$1, { cwd } from "node:process";
12
12
  import { Worker } from "node:worker_threads";
@@ -57,189 +57,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
57
57
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp$14({}, "__esModule", { value: true }), mod);
58
58
  var __require$1 = /* #__PURE__ */ (() => createRequire(import.meta.url))();
59
59
  //#endregion
60
- //#region ../datalake-core/dist/node/index.mjs
61
- var API_ERROR_STATUS$1 = {
62
- bad_request: 400,
63
- unauthorized: 401,
64
- forbidden: 403,
65
- not_found: 404,
66
- conflict: 409,
67
- unprocessable: 422,
68
- rate_limited: 429,
69
- internal: 500
70
- };
71
- function datalakeCollectionPath() {
72
- return `/v1/datalakes`;
73
- }
74
- function datalakeResourcePath(idOrName) {
75
- return `${datalakeCollectionPath()}/${encodeURIComponent(idOrName)}`;
76
- }
77
- function datalakeGrantsPath(idOrName) {
78
- return `${datalakeResourcePath(idOrName)}/grants`;
79
- }
80
- function datalakeGrantPath(idOrName, principal) {
81
- return `${datalakeGrantsPath(idOrName)}/${encodeURIComponent(principal)}`;
82
- }
83
- function datalakeTokensPath(idOrName) {
84
- return `${datalakeResourcePath(idOrName)}/tokens`;
85
- }
86
- function datalakeHealthPath() {
87
- return `/v1/health`;
88
- }
89
- var DATALAKE_HEADER_DUPLICATES = "x-datalake-duplicates";
90
- var DATALAKE_HEADER_REJECTED = "x-datalake-rejected";
91
- var DATALAKE_HEADER_NEXT_CURSOR = "x-datalake-next-cursor";
92
- function base64UrlEncode(input) {
93
- return (typeof input === "string" ? Buffer.from(input) : input).toString("base64url");
94
- }
95
- function base64UrlDecode(input) {
96
- return Buffer.from(input, "base64url");
97
- }
98
- function signSessionToken(input) {
99
- const issuedAt = Math.floor(Date.now() / 1e3);
100
- const expiresAt = issuedAt + input.ttlSeconds;
101
- const payload = {
102
- sub: input.address.toLowerCase(),
103
- typ: "session",
104
- iat: issuedAt,
105
- exp: expiresAt,
106
- jti: randomBytes(8).toString("hex")
107
- };
108
- const toSign = `${base64UrlEncode(JSON.stringify({
109
- alg: "HS256",
110
- typ: "JWT"
111
- }))}.${base64UrlEncode(JSON.stringify(payload))}`;
112
- return {
113
- token: `${toSign}.${createHmac("sha256", input.signingSecret).update(toSign).digest("base64url")}`,
114
- expiresAt: (/* @__PURE__ */ new Date(expiresAt * 1e3)).toISOString()
115
- };
116
- }
117
- var SessionTokenVerificationError = class extends Error {
118
- code;
119
- constructor(code, message) {
120
- super(message);
121
- this.name = "SessionTokenVerificationError";
122
- this.code = code;
123
- }
124
- };
125
- function verifySessionToken(input) {
126
- const parts = input.token.split(".");
127
- if (parts.length !== 3) throw new SessionTokenVerificationError("malformed", "Token is not a JWS compact serialization");
128
- const [header, body, providedSignature] = parts;
129
- const expected = createHmac("sha256", input.signingSecret).update(`${header}.${body}`).digest("base64url");
130
- const expectedBytes = Buffer.from(expected);
131
- const providedBytes = Buffer.from(providedSignature);
132
- if (expectedBytes.length !== providedBytes.length || !timingSafeEqual(expectedBytes, providedBytes)) throw new SessionTokenVerificationError("bad_signature", "Signature mismatch");
133
- let payload;
134
- try {
135
- payload = JSON.parse(base64UrlDecode(body).toString("utf8"));
136
- } catch {
137
- throw new SessionTokenVerificationError("malformed", "Payload is not valid JSON");
138
- }
139
- if (payload.typ !== "session") throw new SessionTokenVerificationError("wrong_type", "Not a session token");
140
- const now = input.now ?? Math.floor(Date.now() / 1e3);
141
- if (payload.exp <= now) throw new SessionTokenVerificationError("expired", "Token has expired");
142
- return payload;
143
- }
144
- function signChallenge(input) {
145
- const issuedAt = Math.floor(Date.now() / 1e3);
146
- const expiresAt = issuedAt + input.ttlSeconds;
147
- const payload = {
148
- address: input.address.toLowerCase(),
149
- typ: "challenge",
150
- iat: issuedAt,
151
- exp: expiresAt,
152
- jti: randomBytes(8).toString("hex")
153
- };
154
- const toSign = `${base64UrlEncode(JSON.stringify({
155
- alg: "HS256",
156
- typ: "JWT"
157
- }))}.${base64UrlEncode(JSON.stringify(payload))}`;
158
- return {
159
- challenge: `${toSign}.${createHmac("sha256", input.signingSecret).update(toSign).digest("base64url")}`,
160
- expiresAt: (/* @__PURE__ */ new Date(expiresAt * 1e3)).toISOString()
161
- };
162
- }
163
- function verifyChallenge(input) {
164
- const parts = input.challenge.split(".");
165
- if (parts.length !== 3) throw new SessionTokenVerificationError("malformed", "Challenge is not a JWS compact serialization");
166
- const [header, body, providedSignature] = parts;
167
- const expected = createHmac("sha256", input.signingSecret).update(`${header}.${body}`).digest("base64url");
168
- const expectedBytes = Buffer.from(expected);
169
- const providedBytes = Buffer.from(providedSignature);
170
- if (expectedBytes.length !== providedBytes.length || !timingSafeEqual(expectedBytes, providedBytes)) throw new SessionTokenVerificationError("bad_signature", "Challenge signature mismatch");
171
- let payload;
172
- try {
173
- payload = JSON.parse(base64UrlDecode(body).toString("utf8"));
174
- } catch {
175
- throw new SessionTokenVerificationError("malformed", "Challenge payload not valid JSON");
176
- }
177
- if (payload.typ !== "challenge") throw new SessionTokenVerificationError("wrong_type", "Not a challenge token");
178
- const now = input.now ?? Math.floor(Date.now() / 1e3);
179
- if (payload.exp <= now) throw new SessionTokenVerificationError("expired", "Challenge has expired");
180
- return payload;
181
- }
182
- function base64UrlEncode2(input) {
183
- return (typeof input === "string" ? Buffer.from(input) : input).toString("base64url");
184
- }
185
- function base64UrlDecode2(input) {
186
- return Buffer.from(input, "base64url");
187
- }
188
- function signToken(input) {
189
- const issuedAt = Math.floor(Date.now() / 1e3);
190
- const expiresAt = issuedAt + input.ttlSeconds;
191
- const payload = {
192
- sub: input.ownerId,
193
- dl: input.datalakeId,
194
- role: input.role,
195
- iat: issuedAt,
196
- exp: expiresAt,
197
- jti: randomBytes(8).toString("hex")
198
- };
199
- const toSign = `${base64UrlEncode2(JSON.stringify({
200
- alg: "HS256",
201
- typ: "JWT"
202
- }))}.${base64UrlEncode2(JSON.stringify(payload))}`;
203
- return {
204
- token: `${toSign}.${createHmac("sha256", input.signingSecret).update(toSign).digest("base64url")}`,
205
- datalakeId: input.datalakeId,
206
- role: input.role,
207
- expiresAt: (/* @__PURE__ */ new Date(expiresAt * 1e3)).toISOString(),
208
- url: input.url
209
- };
210
- }
211
- var TokenVerificationError = class extends Error {
212
- code;
213
- constructor(code, message) {
214
- super(message);
215
- this.name = "TokenVerificationError";
216
- this.code = code;
217
- }
218
- };
219
- function verifyToken(input) {
220
- const parts = input.token.split(".");
221
- if (parts.length !== 3) throw new TokenVerificationError("malformed", "Token is not a JWS compact serialization");
222
- const [header, body, providedSignature] = parts;
223
- const expected = createHmac("sha256", input.signingSecret).update(`${header}.${body}`).digest("base64url");
224
- const expectedBytes = Buffer.from(expected);
225
- const providedBytes = Buffer.from(providedSignature);
226
- if (expectedBytes.length !== providedBytes.length || !timingSafeEqual(expectedBytes, providedBytes)) throw new TokenVerificationError("bad_signature", "Signature mismatch");
227
- let payload;
228
- try {
229
- payload = JSON.parse(base64UrlDecode2(body).toString("utf8"));
230
- } catch {
231
- throw new TokenVerificationError("malformed", "Payload is not valid JSON");
232
- }
233
- const now = input.now ?? Math.floor(Date.now() / 1e3);
234
- if (payload.exp <= now) throw new TokenVerificationError("expired", "Token has expired");
235
- if (payload.iat > now + 60) throw new TokenVerificationError("not_yet_valid", "Token issued in the future");
236
- return payload;
237
- }
238
- var PUBLIC_PRINCIPAL = "public";
239
- function isPublicPrincipal(principal) {
240
- return principal === PUBLIC_PRINCIPAL;
241
- }
242
- //#endregion
243
60
  //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
244
61
  var _a$1;
245
62
  function $constructor(name, initializer, params) {
@@ -37171,10 +36988,10 @@ Object.freeze(scryptSync);
37171
36988
  //#endregion
37172
36989
  //#region ../../node_modules/.pnpm/ethers@6.17.0/node_modules/ethers/lib.esm/crypto/sha2.js
37173
36990
  const _sha256 = function(data) {
37174
- return createHash$3("sha256").update(data).digest();
36991
+ return createHash$2("sha256").update(data).digest();
37175
36992
  };
37176
36993
  const _sha512 = function(data) {
37177
- return createHash$3("sha512").update(data).digest();
36994
+ return createHash$2("sha512").update(data).digest();
37178
36995
  };
37179
36996
  let __sha256 = _sha256;
37180
36997
  let __sha512 = _sha512;
@@ -46561,7 +46378,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
46561
46378
  const http$6 = __require$1("http");
46562
46379
  const net = __require$1("net");
46563
46380
  const tls = __require$1("tls");
46564
- const { randomBytes: randomBytes$3, createHash: createHash$2 } = __require$1("crypto");
46381
+ const { randomBytes: randomBytes$3, createHash: createHash$1 } = __require$1("crypto");
46565
46382
  const { Duplex: Duplex$2, Readable: Readable$3 } = __require$1("stream");
46566
46383
  const { URL: URL$2 } = __require$1("url");
46567
46384
  const PerMessageDeflate = require_permessage_deflate();
@@ -47229,7 +47046,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
47229
47046
  abortHandshake(websocket, socket, "Invalid Upgrade header");
47230
47047
  return;
47231
47048
  }
47232
- const digest = createHash$2("sha1").update(key + GUID).digest("base64");
47049
+ const digest = createHash$1("sha1").update(key + GUID).digest("base64");
47233
47050
  if (res.headers["sec-websocket-accept"] !== digest) {
47234
47051
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
47235
47052
  return;
@@ -47703,7 +47520,7 @@ var require_websocket_server = /* @__PURE__ */ __commonJSMin(((exports, module)
47703
47520
  const EventEmitter$11 = __require$1("events");
47704
47521
  const http$5 = __require$1("http");
47705
47522
  const { Duplex } = __require$1("stream");
47706
- const { createHash: createHash$1 } = __require$1("crypto");
47523
+ const { createHash } = __require$1("crypto");
47707
47524
  const extension = require_extension();
47708
47525
  const PerMessageDeflate = require_permessage_deflate();
47709
47526
  const subprotocol = require_subprotocol();
@@ -47972,7 +47789,7 @@ var require_websocket_server = /* @__PURE__ */ __commonJSMin(((exports, module)
47972
47789
  "HTTP/1.1 101 Switching Protocols",
47973
47790
  "Upgrade: websocket",
47974
47791
  "Connection: Upgrade",
47975
- `Sec-WebSocket-Accept: ${createHash$1("sha1").update(key + GUID).digest("base64")}`
47792
+ `Sec-WebSocket-Accept: ${createHash("sha1").update(key + GUID).digest("base64")}`
47976
47793
  ];
47977
47794
  const ws = new this.options.WebSocket(null, void 0, this.options);
47978
47795
  if (protocols.size) {
@@ -78105,6 +77922,142 @@ var AdhocWitness = class extends AbstractWitness {
78105
77922
  };
78106
77923
  HttpBridge.factory(), ViewArchivist.factory(), ViewNode.factory(), AdhocWitness.factory(), GenericPayloadDiviner.factory(), MemoryBoundWitnessDiviner.factory(), IdentityDiviner.factory(), MemoryArchivist.factory(), MemoryArchivist.factory(), MemoryNode.factory(), MemorySentinel.factory(), GenericPayloadDiviner.factory();
78107
77924
  //#endregion
77925
+ //#region ../jwt/dist/neutral/index.mjs
77926
+ var ADDRESS_PATTERN$1 = /^(?:0x)?[\da-fA-F]{40}$/;
77927
+ var PUBLIC_KEY_PATTERN = /^[\da-fA-F]{128}$/;
77928
+ var DEFAULT_MAX_LIFETIME_SECONDS = 300;
77929
+ var DEFAULT_FUTURE_SKEW_SECONDS = 60;
77930
+ async function verifyWalletJwtPolicy(token, options) {
77931
+ const reasons = [];
77932
+ let decoded;
77933
+ try {
77934
+ decoded = decodeJwt(token);
77935
+ } catch {
77936
+ return {
77937
+ ok: false,
77938
+ reasons: ["token is not a valid compact JWT"]
77939
+ };
77940
+ }
77941
+ const header = asRecord(decoded.header);
77942
+ const payload = asRecord(decoded.payload);
77943
+ if (header === void 0 || payload === void 0) return {
77944
+ ok: false,
77945
+ reasons: ["JWT header and payload must be JSON objects"]
77946
+ };
77947
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
77948
+ const maxLifetimeSeconds = options.maxLifetimeSeconds ?? DEFAULT_MAX_LIFETIME_SECONDS;
77949
+ const futureSkewSeconds = options.futureSkewSeconds ?? DEFAULT_FUTURE_SKEW_SECONDS;
77950
+ validateHeader(header, reasons);
77951
+ validatePayload(payload, options.audience, now, maxLifetimeSeconds, futureSkewSeconds, reasons);
77952
+ const scopes = validateProfile(payload, options.profile, reasons);
77953
+ await validateSignature(token, options.audience, now, reasons);
77954
+ const kid = typeof header.kid === "string" ? header.kid : "";
77955
+ const principal = resolvePrincipal(header, payload, reasons);
77956
+ const uniqueReasons = [...new Set(reasons)];
77957
+ if (uniqueReasons.length > 0 || principal === void 0) return {
77958
+ ok: false,
77959
+ reasons: uniqueReasons.length > 0 ? uniqueReasons : ["wallet principal is invalid"]
77960
+ };
77961
+ return {
77962
+ ok: true,
77963
+ header: {
77964
+ alg: JwtAlg.ES256K,
77965
+ typ: JwtTyp.JWT,
77966
+ kid,
77967
+ pub: header.pub
77968
+ },
77969
+ payload,
77970
+ principal,
77971
+ scopes
77972
+ };
77973
+ }
77974
+ async function validateSignature(token, audience, now, reasons) {
77975
+ try {
77976
+ const verified = await verifyJwt(token, {
77977
+ audience,
77978
+ now
77979
+ });
77980
+ if (!verified.ok) reasons.push(...verified.reasons);
77981
+ } catch {
77982
+ reasons.push("wallet JWT cryptographic verification failed");
77983
+ }
77984
+ }
77985
+ function resolvePrincipal(header, payload, reasons) {
77986
+ const kid = typeof header.kid === "string" ? header.kid : "";
77987
+ const issuer = typeof payload.iss === "string" ? payload.iss : "";
77988
+ if (!ADDRESS_PATTERN$1.test(kid) || !ADDRESS_PATTERN$1.test(issuer)) return void 0;
77989
+ try {
77990
+ const normalizedKid = normalizeAddress(kid);
77991
+ if (normalizedKid === normalizeAddress(issuer)) return normalizedKid;
77992
+ reasons.push("header.kid does not match payload.iss");
77993
+ } catch {
77994
+ reasons.push("kid or iss is not a valid XYO address");
77995
+ }
77996
+ }
77997
+ function validateHeader(header, reasons) {
77998
+ if (header.alg !== JwtAlg.ES256K) reasons.push("header.alg must be ES256K");
77999
+ if (header.typ !== JwtTyp.JWT) reasons.push("header.typ must be JWT");
78000
+ if (typeof header.kid !== "string" || !ADDRESS_PATTERN$1.test(header.kid)) reasons.push("header.kid must be a 20-byte hex address");
78001
+ if (typeof header.pub !== "string" || !PUBLIC_KEY_PATTERN.test(header.pub)) reasons.push("header.pub must be an embedded 64-byte secp256k1 public key");
78002
+ }
78003
+ function validatePayload(payload, audience, now, maxLifetimeSeconds, futureSkewSeconds, reasons) {
78004
+ if (typeof payload.iss !== "string" || !ADDRESS_PATTERN$1.test(payload.iss)) reasons.push("payload.iss must be a 20-byte hex address");
78005
+ if (payload.aud !== audience) reasons.push("payload.aud does not exactly match the required audience");
78006
+ if (payload.schema !== JwtSchema.Signin) reasons.push(`payload.schema must be ${JwtSchema.Signin}`);
78007
+ const iat = payload.iat;
78008
+ const exp = payload.exp;
78009
+ if (!isFiniteInteger$1(iat)) reasons.push("payload.iat must be a finite integer");
78010
+ if (!isFiniteInteger$1(exp)) reasons.push("payload.exp must be a finite integer");
78011
+ if (isFiniteInteger$1(iat) && isFiniteInteger$1(exp)) {
78012
+ if (exp <= iat) reasons.push("payload.exp must be greater than payload.iat");
78013
+ if (exp - iat > maxLifetimeSeconds) reasons.push(`wallet JWT lifetime exceeds ${maxLifetimeSeconds} seconds`);
78014
+ if (iat > now + futureSkewSeconds) reasons.push(`payload.iat is more than ${futureSkewSeconds} seconds in the future`);
78015
+ if (exp <= now) reasons.push("wallet JWT has expired");
78016
+ }
78017
+ if (payload.nbf !== void 0) {
78018
+ if (!isFiniteInteger$1(payload.nbf)) reasons.push("payload.nbf must be a finite integer");
78019
+ else if (payload.nbf > now) reasons.push("wallet JWT is not yet valid");
78020
+ }
78021
+ }
78022
+ function validateProfile(payload, profile, reasons) {
78023
+ if (profile.kind === "service") {
78024
+ if (payload.origin !== void 0 || payload.scope !== void 0) reasons.push("browser origin/scope claims are not accepted by the service JWT profile");
78025
+ return [];
78026
+ }
78027
+ const origin = profile.origin;
78028
+ if (origin === void 0 || origin.length === 0) reasons.push("browser request Origin is required");
78029
+ if (origin !== void 0 && !profile.allowedOrigins.includes(origin)) reasons.push("browser request Origin is not allowed");
78030
+ if (typeof payload.origin !== "string" || payload.origin.length === 0) reasons.push("payload.origin is required for browser JWTs");
78031
+ else {
78032
+ if (payload.origin !== origin) reasons.push("payload.origin does not match the request Origin");
78033
+ if (!profile.allowedOrigins.includes(payload.origin)) reasons.push("payload.origin is not allowed");
78034
+ }
78035
+ return parseCanonicalScope(payload.scope, profile.allowedScopes, reasons);
78036
+ }
78037
+ function parseCanonicalScope(value, allowedScopes, reasons) {
78038
+ if (typeof value !== "string" || value.length === 0) {
78039
+ reasons.push("payload.scope is required for browser JWTs");
78040
+ return [];
78041
+ }
78042
+ const scopes = value.split(" ");
78043
+ if (scopes.some((scope) => scope.length === 0)) {
78044
+ reasons.push("payload.scope must use single-space separators with no surrounding whitespace");
78045
+ return [];
78046
+ }
78047
+ const canonical = [...new Set(scopes)].sort();
78048
+ if (canonical.length !== scopes.length || canonical.join(" ") !== value) reasons.push("payload.scope must be sorted, unique, and space-delimited");
78049
+ const allowed = new Set(allowedScopes);
78050
+ for (const scope of scopes) if (!allowed.has(scope)) reasons.push(`payload.scope contains unknown operation: ${scope}`);
78051
+ return scopes;
78052
+ }
78053
+ function isFiniteInteger$1(value) {
78054
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
78055
+ }
78056
+ function asRecord(value) {
78057
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
78058
+ return value;
78059
+ }
78060
+ //#endregion
78108
78061
  //#region ../../node_modules/.pnpm/prom-client@15.1.3/node_modules/prom-client/lib/util.js
78109
78062
  var require_util$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
78110
78063
  exports.getValueAsString = function getValueString(value) {
@@ -80782,24 +80735,10 @@ var import_prom_client = (/* @__PURE__ */ __commonJSMin(((exports) => {
80782
80735
  exports.aggregators = require_metricAggregators().aggregators;
80783
80736
  exports.AggregatorRegistry = require_cluster();
80784
80737
  })))();
80785
- var SelfSignedWalletJwtTokenStore = class {
80786
- audience;
80787
- constructor(audience) {
80788
- this.audience = audience;
80789
- }
80790
- async resolve(token) {
80791
- let alg;
80792
- try {
80793
- alg = decodeJwt(token).header.alg;
80794
- } catch {
80795
- return;
80796
- }
80797
- if (alg !== JwtAlg.ES256K) return void 0;
80798
- const result = await verifyJwt(token, { audience: this.audience });
80799
- if (!result.ok) return void 0;
80800
- return normalizeAddress(result.payload.iss);
80801
- }
80802
- };
80738
+ var TOKEN_REJECTED = Object.freeze({ rejected: true });
80739
+ function isRejectedToken(value) {
80740
+ return typeof value === "object" && value !== null && "rejected" in value && value.rejected === true;
80741
+ }
80803
80742
  var StaticTokenStore = class _StaticTokenStore {
80804
80743
  tokens;
80805
80744
  constructor(tokens) {
@@ -80825,14 +80764,62 @@ var ChainedTokenStore = class {
80825
80764
  constructor(stores) {
80826
80765
  this.stores = stores;
80827
80766
  }
80828
- async resolve(token) {
80767
+ async resolve(token, context) {
80829
80768
  for (const store of this.stores) {
80830
- const result = await store.resolve(token);
80769
+ const result = await store.resolve(token, context);
80770
+ if (isRejectedToken(result)) return result;
80831
80771
  if (result !== void 0) return result;
80832
80772
  }
80833
80773
  }
80834
80774
  };
80835
- var API_ERROR_STATUS = {
80775
+ var SelfSignedWalletJwtTokenStore = class {
80776
+ audience;
80777
+ options;
80778
+ constructor(audience, options = {}) {
80779
+ this.audience = audience;
80780
+ this.options = options;
80781
+ }
80782
+ async resolve(token, context = {}) {
80783
+ if (!isWalletJwtCandidate(token)) return void 0;
80784
+ const isBrowser = context.origin !== void 0 || hasBrowserClaims$1(token);
80785
+ const browser = this.options.browser;
80786
+ if (isBrowser && browser === void 0) return TOKEN_REJECTED;
80787
+ const isServiceProfileAllowed = this.options.isServiceProfileAllowed ?? browser === void 0;
80788
+ if (!isBrowser && !isServiceProfileAllowed) return TOKEN_REJECTED;
80789
+ const result = await verifyWalletJwtPolicy(token, {
80790
+ audience: this.audience,
80791
+ profile: isBrowser ? {
80792
+ kind: "browser",
80793
+ origin: context.origin,
80794
+ allowedOrigins: browser?.allowedOrigins ?? [],
80795
+ allowedScopes: browser?.allowedScopes ?? []
80796
+ } : { kind: "service" }
80797
+ });
80798
+ if (!result.ok) return TOKEN_REJECTED;
80799
+ return {
80800
+ userId: result.principal,
80801
+ kind: "wallet-jwt",
80802
+ profile: isBrowser ? "browser" : "service",
80803
+ scopes: result.scopes
80804
+ };
80805
+ }
80806
+ };
80807
+ function isWalletJwtCandidate(token) {
80808
+ try {
80809
+ return decodeJwt(token).header.alg === "ES256K" || hasBrowserClaims$1(token);
80810
+ } catch {
80811
+ return false;
80812
+ }
80813
+ }
80814
+ function hasBrowserClaims$1(token) {
80815
+ try {
80816
+ const payload = decodeJwt(token).payload;
80817
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload) && ("origin" in payload || "scope" in payload);
80818
+ } catch {
80819
+ return false;
80820
+ }
80821
+ }
80822
+ var API_ERROR_STATUS$1 = {
80836
80823
  bad_request: 400,
80837
80824
  unauthorized: 401,
80838
80825
  forbidden: 403,
@@ -80849,7 +80836,7 @@ var HttpError = class extends Error {
80849
80836
  super(message);
80850
80837
  this.name = "HttpError";
80851
80838
  this.code = code;
80852
- this.statusCode = API_ERROR_STATUS[code];
80839
+ this.statusCode = API_ERROR_STATUS$1[code];
80853
80840
  }
80854
80841
  toErrorBody(requestId) {
80855
80842
  return {
@@ -80867,11 +80854,93 @@ function registerAuth(app, tokenStore, publicPaths, options = {}) {
80867
80854
  const authorization = request.headers.authorization;
80868
80855
  if (!authorization?.toLowerCase().startsWith("bearer ")) throw new HttpError("unauthorized", "Missing or malformed Authorization header");
80869
80856
  const token = authorization.slice(7).trim();
80870
- const userId = await tokenStore.resolve(token);
80871
- if (!userId) throw new HttpError("unauthorized", "Invalid or expired token");
80872
- request.userId = userId;
80857
+ const resolution = await tokenStore.resolve(token, { origin: request.headers.origin });
80858
+ if (!resolution || isRejectedToken(resolution)) throw new HttpError("unauthorized", "Invalid or expired token");
80859
+ const authenticatedUser = typeof resolution === "string" ? {
80860
+ userId: resolution,
80861
+ kind: "opaque",
80862
+ profile: "service",
80863
+ scopes: []
80864
+ } : resolution;
80865
+ if (request.headers.origin !== void 0 && (authenticatedUser.kind !== "wallet-jwt" || authenticatedUser.profile !== "browser")) throw new HttpError("unauthorized", "Browser requests require an origin-bound wallet JWT");
80866
+ request.authenticatedUser = authenticatedUser;
80867
+ request.userId = authenticatedUser.userId;
80873
80868
  });
80874
80869
  }
80870
+ var ALLOWED_METHODS = [
80871
+ "GET",
80872
+ "POST",
80873
+ "DELETE",
80874
+ "OPTIONS"
80875
+ ];
80876
+ var ALLOWED_REQUEST_HEADERS = [
80877
+ "Authorization",
80878
+ "Content-Type",
80879
+ "Accept"
80880
+ ];
80881
+ function parseCorsOrigins(value) {
80882
+ if (value === void 0 || value.trim().length === 0) return [];
80883
+ const origins = value.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
80884
+ const unique = [...new Set(origins)];
80885
+ for (const origin of unique) {
80886
+ let parsed;
80887
+ try {
80888
+ parsed = new URL(origin);
80889
+ } catch {
80890
+ throw new Error(`Invalid CORS origin: ${origin}`);
80891
+ }
80892
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.origin !== origin) throw new Error(`CORS origin must be an exact HTTP(S) origin with no path: ${origin}`);
80893
+ }
80894
+ return unique;
80895
+ }
80896
+ function registerExactCors(app, options) {
80897
+ if (options.allowedOrigins.length === 0) return;
80898
+ const allowed = new Set(options.allowedOrigins);
80899
+ app.addHook("onRequest", async (request, reply) => {
80900
+ const origin = request.headers.origin;
80901
+ setVaryOrigin(reply);
80902
+ if (origin === void 0) return;
80903
+ if (!allowed.has(origin)) {
80904
+ reply.code(403).send({
80905
+ code: "forbidden",
80906
+ message: "Origin is not allowed"
80907
+ });
80908
+ return;
80909
+ }
80910
+ setCorsResponseHeaders(reply, origin, options.exposedHeaders ?? []);
80911
+ if (request.method !== "OPTIONS") return;
80912
+ if (!isValidPreflight(request)) {
80913
+ reply.code(400).send({
80914
+ code: "bad_request",
80915
+ message: "Invalid CORS preflight request"
80916
+ });
80917
+ return;
80918
+ }
80919
+ reply.code(204).send();
80920
+ });
80921
+ }
80922
+ function isValidPreflight(request) {
80923
+ const requestedMethod = request.headers["access-control-request-method"];
80924
+ if (requestedMethod === void 0 || !ALLOWED_METHODS.includes(requestedMethod.toUpperCase())) return false;
80925
+ const requestedHeaders = request.headers["access-control-request-headers"];
80926
+ if (requestedHeaders === void 0) return true;
80927
+ const allowed = new Set(ALLOWED_REQUEST_HEADERS.map((header) => header.toLowerCase()));
80928
+ return requestedHeaders.split(",").map((header) => header.trim().toLowerCase()).filter((header) => header.length > 0).every((header) => allowed.has(header));
80929
+ }
80930
+ function setCorsResponseHeaders(reply, origin, exposedHeaders) {
80931
+ reply.header("access-control-allow-origin", origin);
80932
+ reply.header("access-control-allow-methods", ALLOWED_METHODS.join(", "));
80933
+ reply.header("access-control-allow-headers", ALLOWED_REQUEST_HEADERS.join(", "));
80934
+ if (exposedHeaders.length > 0) reply.header("access-control-expose-headers", exposedHeaders.join(", "));
80935
+ }
80936
+ function setVaryOrigin(reply) {
80937
+ const existing = reply.getHeader("vary");
80938
+ if (existing === void 0) {
80939
+ reply.header("vary", "Origin");
80940
+ return;
80941
+ }
80942
+ if (!String(existing).split(",").map((value) => value.trim().toLowerCase()).includes("origin")) reply.header("vary", `${String(existing)}, Origin`);
80943
+ }
80875
80944
  function registerErrorHandler(app) {
80876
80945
  app.setErrorHandler((error, request, reply) => {
80877
80946
  const requestId = request.id;
@@ -80939,7 +81008,7 @@ function registerMetrics$1(app, options) {
80939
81008
  registers: [registry]
80940
81009
  });
80941
81010
  app.addHook("onResponse", (request, reply, done) => {
80942
- const route = (request.routeOptions?.url ?? request.url ?? "unknown").split("?", 1)[0] ?? "unknown";
81011
+ const route = request.routeOptions?.url?.split("?", 1)[0] ?? "unmatched";
80943
81012
  const labels = {
80944
81013
  method: request.method,
80945
81014
  route,
@@ -80995,6 +81064,287 @@ var JsonSnapshotFile = class {
80995
81064
  }
80996
81065
  };
80997
81066
  //#endregion
81067
+ //#region ../datalake-core/dist/node/index.mjs
81068
+ var API_ERROR_STATUS = {
81069
+ bad_request: 400,
81070
+ unauthorized: 401,
81071
+ forbidden: 403,
81072
+ not_found: 404,
81073
+ conflict: 409,
81074
+ unprocessable: 422,
81075
+ rate_limited: 429,
81076
+ internal: 500
81077
+ };
81078
+ function datalakeCollectionPath() {
81079
+ return `/v1/datalakes`;
81080
+ }
81081
+ function datalakeResourcePath(idOrName) {
81082
+ return `${datalakeCollectionPath()}/${encodeURIComponent(idOrName)}`;
81083
+ }
81084
+ function datalakeGrantsPath(idOrName) {
81085
+ return `${datalakeResourcePath(idOrName)}/grants`;
81086
+ }
81087
+ function datalakeGrantPath(idOrName, principal) {
81088
+ return `${datalakeGrantsPath(idOrName)}/${encodeURIComponent(principal)}`;
81089
+ }
81090
+ function datalakeTokensPath(idOrName) {
81091
+ return `${datalakeResourcePath(idOrName)}/tokens`;
81092
+ }
81093
+ function datalakeHealthPath() {
81094
+ return `/v1/health`;
81095
+ }
81096
+ var DATALAKE_HEADER_DUPLICATES = "x-datalake-duplicates";
81097
+ var DATALAKE_HEADER_REJECTED = "x-datalake-rejected";
81098
+ var DATALAKE_HEADER_NEXT_CURSOR = "x-datalake-next-cursor";
81099
+ var StrictHs256Error = class extends Error {
81100
+ code;
81101
+ constructor(code, message) {
81102
+ super(message);
81103
+ this.name = "StrictHs256Error";
81104
+ this.code = code;
81105
+ }
81106
+ };
81107
+ function verifyStrictHs256(token, signingSecret) {
81108
+ const parts = token.split(".");
81109
+ if (parts.length !== 3) throw new StrictHs256Error("malformed", "Token is not a JWS compact serialization");
81110
+ const [headerPart, payloadPart, signaturePart] = parts;
81111
+ const header = decodeJsonObject(headerPart, "Header");
81112
+ assertExactKeys(header, ["alg", "typ"], "Header");
81113
+ if (header.alg !== "HS256") throw new StrictHs256Error("malformed", "Header alg must be HS256");
81114
+ if (header.typ !== "JWT") throw new StrictHs256Error("malformed", "Header typ must be JWT");
81115
+ const provided = decodeBase64Url(signaturePart, "Signature");
81116
+ const expected = createHmac("sha256", signingSecret).update(`${headerPart}.${payloadPart}`).digest();
81117
+ if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) throw new StrictHs256Error("bad_signature", "Signature mismatch");
81118
+ return { payload: decodeJsonObject(payloadPart, "Payload") };
81119
+ }
81120
+ function assertExactKeys(value, expected, label) {
81121
+ const actual = Object.keys(value).sort();
81122
+ const canonical = [...expected].sort();
81123
+ if (actual.length !== canonical.length || actual.some((key, index) => key !== canonical[index])) throw new StrictHs256Error("malformed", `${label} fields must be exactly: ${canonical.join(", ")}`);
81124
+ }
81125
+ function isFiniteInteger(value) {
81126
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
81127
+ }
81128
+ function isNonEmptyString(value) {
81129
+ return typeof value === "string" && value.length > 0;
81130
+ }
81131
+ function decodeJsonObject(value, label) {
81132
+ let parsed;
81133
+ try {
81134
+ parsed = JSON.parse(decodeBase64Url(value, label).toString("utf8"));
81135
+ } catch (error) {
81136
+ if (error instanceof StrictHs256Error) throw error;
81137
+ throw new StrictHs256Error("malformed", `${label} is not valid JSON`);
81138
+ }
81139
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new StrictHs256Error("malformed", `${label} must be a JSON object`);
81140
+ return parsed;
81141
+ }
81142
+ function decodeBase64Url(value, label) {
81143
+ if (value.length === 0 || !/^[\w-]+$/.test(value)) throw new StrictHs256Error("malformed", `${label} is not canonical base64url`);
81144
+ const decoded = Buffer.from(value, "base64url");
81145
+ if (decoded.toString("base64url") !== value) throw new StrictHs256Error("malformed", `${label} is not canonical base64url`);
81146
+ return decoded;
81147
+ }
81148
+ function base64UrlEncode(input) {
81149
+ return (typeof input === "string" ? Buffer.from(input) : input).toString("base64url");
81150
+ }
81151
+ function signSessionToken(input) {
81152
+ const issuedAt = Math.floor(Date.now() / 1e3);
81153
+ const expiresAt = issuedAt + input.ttlSeconds;
81154
+ const payload = {
81155
+ sub: input.address.toLowerCase(),
81156
+ typ: "session",
81157
+ iat: issuedAt,
81158
+ exp: expiresAt,
81159
+ jti: randomBytes(8).toString("hex")
81160
+ };
81161
+ const toSign = `${base64UrlEncode(JSON.stringify({
81162
+ alg: "HS256",
81163
+ typ: "JWT"
81164
+ }))}.${base64UrlEncode(JSON.stringify(payload))}`;
81165
+ return {
81166
+ token: `${toSign}.${createHmac("sha256", input.signingSecret).update(toSign).digest("base64url")}`,
81167
+ expiresAt: (/* @__PURE__ */ new Date(expiresAt * 1e3)).toISOString()
81168
+ };
81169
+ }
81170
+ var SessionTokenVerificationError = class extends Error {
81171
+ code;
81172
+ constructor(code, message) {
81173
+ super(message);
81174
+ this.name = "SessionTokenVerificationError";
81175
+ this.code = code;
81176
+ }
81177
+ };
81178
+ function verifySessionToken(input) {
81179
+ let raw;
81180
+ try {
81181
+ raw = verifyStrictHs256(input.token, input.signingSecret).payload;
81182
+ assertExactKeys(raw, [
81183
+ "exp",
81184
+ "iat",
81185
+ "jti",
81186
+ "sub",
81187
+ "typ"
81188
+ ], "Payload");
81189
+ } catch (error) {
81190
+ if (error instanceof StrictHs256Error) throw new SessionTokenVerificationError(error.code, error.message);
81191
+ throw error;
81192
+ }
81193
+ if (raw.typ !== "session") throw new SessionTokenVerificationError("wrong_type", "Not a session token");
81194
+ if (!isNonEmptyString(raw.sub)) throw new SessionTokenVerificationError("malformed", "Payload sub must be a non-empty string");
81195
+ if (!isNonEmptyString(raw.jti)) throw new SessionTokenVerificationError("malformed", "Payload jti must be a non-empty string");
81196
+ if (!isFiniteInteger(raw.iat)) throw new SessionTokenVerificationError("malformed", "Payload iat must be a finite integer");
81197
+ if (!isFiniteInteger(raw.exp)) throw new SessionTokenVerificationError("malformed", "Payload exp must be a finite integer");
81198
+ if (raw.exp <= raw.iat) throw new SessionTokenVerificationError("malformed", "Payload exp must be greater than iat");
81199
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
81200
+ if (raw.exp <= now) throw new SessionTokenVerificationError("expired", "Token has expired");
81201
+ if (raw.iat > now + 60) throw new SessionTokenVerificationError("not_yet_valid", "Token issued in the future");
81202
+ return {
81203
+ sub: raw.sub,
81204
+ typ: raw.typ,
81205
+ iat: raw.iat,
81206
+ exp: raw.exp,
81207
+ jti: raw.jti
81208
+ };
81209
+ }
81210
+ function signChallenge(input) {
81211
+ const issuedAt = Math.floor(Date.now() / 1e3);
81212
+ const expiresAt = issuedAt + input.ttlSeconds;
81213
+ const payload = {
81214
+ address: input.address.toLowerCase(),
81215
+ typ: "challenge",
81216
+ iat: issuedAt,
81217
+ exp: expiresAt,
81218
+ jti: randomBytes(8).toString("hex")
81219
+ };
81220
+ const toSign = `${base64UrlEncode(JSON.stringify({
81221
+ alg: "HS256",
81222
+ typ: "JWT"
81223
+ }))}.${base64UrlEncode(JSON.stringify(payload))}`;
81224
+ return {
81225
+ challenge: `${toSign}.${createHmac("sha256", input.signingSecret).update(toSign).digest("base64url")}`,
81226
+ expiresAt: (/* @__PURE__ */ new Date(expiresAt * 1e3)).toISOString()
81227
+ };
81228
+ }
81229
+ function verifyChallenge(input) {
81230
+ let raw;
81231
+ try {
81232
+ raw = verifyStrictHs256(input.challenge, input.signingSecret).payload;
81233
+ assertExactKeys(raw, [
81234
+ "address",
81235
+ "exp",
81236
+ "iat",
81237
+ "jti",
81238
+ "typ"
81239
+ ], "Challenge payload");
81240
+ } catch (error) {
81241
+ if (error instanceof StrictHs256Error) throw new SessionTokenVerificationError(error.code, error.message);
81242
+ throw error;
81243
+ }
81244
+ if (raw.typ !== "challenge") throw new SessionTokenVerificationError("wrong_type", "Not a challenge token");
81245
+ if (!isNonEmptyString(raw.address) || !/^(?:0x)?[\da-fA-F]{40}$/.test(raw.address)) throw new SessionTokenVerificationError("malformed", "Challenge address must be a 20-byte hex address");
81246
+ if (!isNonEmptyString(raw.jti)) throw new SessionTokenVerificationError("malformed", "Challenge jti must be a non-empty string");
81247
+ if (!isFiniteInteger(raw.iat)) throw new SessionTokenVerificationError("malformed", "Challenge iat must be a finite integer");
81248
+ if (!isFiniteInteger(raw.exp)) throw new SessionTokenVerificationError("malformed", "Challenge exp must be a finite integer");
81249
+ if (raw.exp <= raw.iat) throw new SessionTokenVerificationError("malformed", "Challenge exp must be greater than iat");
81250
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
81251
+ if (raw.exp <= now) throw new SessionTokenVerificationError("expired", "Challenge has expired");
81252
+ if (raw.iat > now + 60) throw new SessionTokenVerificationError("not_yet_valid", "Challenge issued in the future");
81253
+ return {
81254
+ address: raw.address,
81255
+ typ: raw.typ,
81256
+ iat: raw.iat,
81257
+ exp: raw.exp,
81258
+ jti: raw.jti
81259
+ };
81260
+ }
81261
+ function base64UrlEncode2(input) {
81262
+ return (typeof input === "string" ? Buffer.from(input) : input).toString("base64url");
81263
+ }
81264
+ function signToken(input) {
81265
+ const issuedAt = Math.floor(Date.now() / 1e3);
81266
+ const expiresAt = issuedAt + input.ttlSeconds;
81267
+ const payload = {
81268
+ sub: input.ownerId,
81269
+ dl: input.datalakeId,
81270
+ role: input.role,
81271
+ iat: issuedAt,
81272
+ exp: expiresAt,
81273
+ jti: randomBytes(8).toString("hex")
81274
+ };
81275
+ const toSign = `${base64UrlEncode2(JSON.stringify({
81276
+ alg: "HS256",
81277
+ typ: "JWT"
81278
+ }))}.${base64UrlEncode2(JSON.stringify(payload))}`;
81279
+ return {
81280
+ token: `${toSign}.${createHmac("sha256", input.signingSecret).update(toSign).digest("base64url")}`,
81281
+ datalakeId: input.datalakeId,
81282
+ role: input.role,
81283
+ expiresAt: (/* @__PURE__ */ new Date(expiresAt * 1e3)).toISOString(),
81284
+ url: input.url
81285
+ };
81286
+ }
81287
+ var TokenVerificationError = class extends Error {
81288
+ code;
81289
+ constructor(code, message) {
81290
+ super(message);
81291
+ this.name = "TokenVerificationError";
81292
+ this.code = code;
81293
+ }
81294
+ };
81295
+ function verifyToken(input) {
81296
+ let raw;
81297
+ try {
81298
+ raw = verifyStrictHs256(input.token, input.signingSecret).payload;
81299
+ assertExactKeys(raw, [
81300
+ "dl",
81301
+ "exp",
81302
+ "iat",
81303
+ "jti",
81304
+ "role",
81305
+ "sub"
81306
+ ], "Payload");
81307
+ } catch (error) {
81308
+ if (error instanceof StrictHs256Error) throw new TokenVerificationError(error.code, error.message);
81309
+ throw error;
81310
+ }
81311
+ if (!isNonEmptyString(raw.sub)) throw new TokenVerificationError("malformed", "Payload sub must be a non-empty string");
81312
+ if (!isNonEmptyString(raw.dl)) throw new TokenVerificationError("malformed", "Payload dl must be a non-empty string");
81313
+ if (raw.role !== "viewer" && raw.role !== "runner") throw new TokenVerificationError("malformed", "Payload role must be viewer or runner");
81314
+ if (!isFiniteInteger(raw.iat)) throw new TokenVerificationError("malformed", "Payload iat must be a finite integer");
81315
+ if (!isFiniteInteger(raw.exp)) throw new TokenVerificationError("malformed", "Payload exp must be a finite integer");
81316
+ if (!isNonEmptyString(raw.jti)) throw new TokenVerificationError("malformed", "Payload jti must be a non-empty string");
81317
+ if (raw.exp <= raw.iat) throw new TokenVerificationError("malformed", "Payload exp must be greater than iat");
81318
+ const payload = {
81319
+ sub: raw.sub,
81320
+ dl: raw.dl,
81321
+ role: raw.role,
81322
+ iat: raw.iat,
81323
+ exp: raw.exp,
81324
+ jti: raw.jti
81325
+ };
81326
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
81327
+ if (payload.exp <= now) throw new TokenVerificationError("expired", "Token has expired");
81328
+ if (payload.iat > now + 60) throw new TokenVerificationError("not_yet_valid", "Token issued in the future");
81329
+ return payload;
81330
+ }
81331
+ var PUBLIC_PRINCIPAL = "public";
81332
+ function isPublicPrincipal(principal) {
81333
+ return principal === PUBLIC_PRINCIPAL;
81334
+ }
81335
+ var DATALAKE_CONTROL_OPERATIONS = [
81336
+ "datalake:create",
81337
+ "datalake:describe",
81338
+ "datalake:list"
81339
+ ];
81340
+ var DATALAKE_DATA_OPERATIONS = [
81341
+ "payload:append",
81342
+ "payload:read",
81343
+ "usage:read"
81344
+ ];
81345
+ DATALAKE_CONTROL_OPERATIONS.join(" ");
81346
+ DATALAKE_DATA_OPERATIONS.join(" ");
81347
+ //#endregion
80998
81348
  //#region ../../node_modules/.pnpm/reusify@1.1.0/node_modules/reusify/reusify.js
80999
81349
  var require_reusify = /* @__PURE__ */ __commonJSMin(((exports, module) => {
81000
81350
  function reusify(Constructor) {
@@ -111141,6 +111491,7 @@ function registerAuthRoutes(app, deps) {
111141
111491
  const challengeTtl = deps.challengeTtlSeconds ?? 300;
111142
111492
  const sessionTtl = deps.sessionTtlSeconds ?? 1440 * 60;
111143
111493
  app.post(AUTH_CHALLENGE_PATH, async (request) => {
111494
+ requireServiceOnlyRequest(request);
111144
111495
  const address = request.body?.address;
111145
111496
  if (!address || !isLikelyAddress(address)) throw new HttpError("bad_request", "address is required (0x-prefixed 20-byte hex)");
111146
111497
  const { challenge, expiresAt } = signChallenge({
@@ -111154,6 +111505,7 @@ function registerAuthRoutes(app, deps) {
111154
111505
  };
111155
111506
  });
111156
111507
  app.post(AUTH_VERIFY_PATH, async (request) => {
111508
+ requireServiceOnlyRequest(request);
111157
111509
  const { address, challenge, signature } = request.body ?? {};
111158
111510
  if (!address || !challenge || !signature) throw new HttpError("bad_request", "address, challenge, and signature are required");
111159
111511
  if (!isLikelyAddress(address)) throw new HttpError("bad_request", "address must be 0x-prefixed 20-byte hex");
@@ -111187,6 +111539,9 @@ function registerAuthRoutes(app, deps) {
111187
111539
  };
111188
111540
  });
111189
111541
  }
111542
+ function requireServiceOnlyRequest(request) {
111543
+ if (request.headers.origin !== void 0) throw new HttpError("forbidden", "Legacy challenge/session authentication is service-only");
111544
+ }
111190
111545
  function isLikelyAddress(value) {
111191
111546
  return /^0x[a-fA-F0-9]{40}$/.test(value);
111192
111547
  }
@@ -111197,6 +111552,7 @@ function registerDatalakeRoutes(app, deps) {
111197
111552
  const idGenerator = deps.idGenerator ?? defaultIdGenerator;
111198
111553
  const originUrl = deps.datalakeOriginUrl.replace(/\/$/, "");
111199
111554
  app.post(datalakeCollectionPath(), async (request) => {
111555
+ requireControlOperation(request, "datalake:create");
111200
111556
  const body = request.body;
111201
111557
  if (!body || typeof body.name !== "string" || !body.config) throw new HttpError("bad_request", "name and config are required");
111202
111558
  if (await deps.store.findByIdOrName(request.userId, body.name)) throw new HttpError("conflict", `Datalake already exists: ${body.name}`);
@@ -111211,21 +111567,25 @@ function registerDatalakeRoutes(app, deps) {
111211
111567
  return descriptor;
111212
111568
  });
111213
111569
  app.get(datalakeCollectionPath(), async (request) => {
111570
+ requireControlOperation(request, "datalake:list");
111214
111571
  return deps.store.listByOwner(request.userId);
111215
111572
  });
111216
111573
  app.get(`${datalakeCollectionPath()}/:idOrName`, async (request) => {
111574
+ requireControlOperation(request, "datalake:describe");
111217
111575
  return requireDatalake(deps, request.userId, request.params.idOrName);
111218
111576
  });
111219
111577
  app.delete(`${datalakeCollectionPath()}/:idOrName`, async (request, reply) => {
111578
+ requireControlOperation(request);
111220
111579
  const descriptor = await requireDatalake(deps, request.userId, request.params.idOrName);
111221
111580
  await deps.store.remove(descriptor.id);
111222
111581
  reply.code(204).send();
111223
111582
  });
111224
111583
  app.post(`${datalakeResourcePath(":idOrName")}/grants`.replaceAll("%3A", ":"), async (request) => {
111584
+ requireControlOperation(request);
111225
111585
  const body = request.body;
111226
111586
  if (!body || typeof body.principal !== "string" || !body.role) throw new HttpError("bad_request", "principal and role are required");
111227
111587
  if (body.role !== "viewer" && body.role !== "runner") throw new HttpError("unprocessable", `Invalid role: ${String(body.role)}`);
111228
- const principal = normalizePrincipal(body.principal);
111588
+ const principal = normalizePrincipal$1(body.principal);
111229
111589
  const descriptor = await requireDatalake(deps, request.userId, request.params.idOrName);
111230
111590
  return deps.store.upsertAcl(descriptor.id, {
111231
111591
  principal,
@@ -111236,12 +111596,14 @@ function registerDatalakeRoutes(app, deps) {
111236
111596
  });
111237
111597
  const grantPathPattern = datalakeGrantPath(":idOrName", ":principal").replaceAll("%3A", ":");
111238
111598
  app.delete(grantPathPattern, async (request) => {
111239
- const principal = normalizePrincipal(request.params.principal);
111599
+ requireControlOperation(request);
111600
+ const principal = normalizePrincipal$1(request.params.principal);
111240
111601
  const descriptor = await requireDatalake(deps, request.userId, request.params.idOrName);
111241
111602
  return deps.store.removeAcl(descriptor.id, principal);
111242
111603
  });
111243
111604
  const tokensPathPattern = datalakeTokensPath(":idOrName").replaceAll("%3A", ":");
111244
111605
  app.post(tokensPathPattern, async (request) => {
111606
+ requireControlOperation(request);
111245
111607
  const body = request.body;
111246
111608
  if (!body?.role) throw new HttpError("bad_request", "role is required");
111247
111609
  if (body.role !== "viewer" && body.role !== "runner") throw new HttpError("unprocessable", `Invalid role: ${String(body.role)}`);
@@ -111260,19 +111622,39 @@ function registerDatalakeRoutes(app, deps) {
111260
111622
  });
111261
111623
  });
111262
111624
  }
111625
+ function requireControlOperation(request, operation) {
111626
+ const auth = request.authenticatedUser;
111627
+ if (request.headers.origin !== void 0 && (auth.kind !== "wallet-jwt" || auth.profile !== "browser")) throw new HttpError("forbidden", "Browser requests require an origin-bound wallet JWT");
111628
+ if (auth.kind !== "wallet-jwt" || auth.profile !== "browser") return;
111629
+ if (operation === void 0 || !auth.scopes.includes(operation)) throw new HttpError("forbidden", "Browser wallet JWT does not authorize this operation");
111630
+ }
111263
111631
  async function requireDatalake(deps, ownerId, idOrName) {
111264
111632
  const descriptor = await deps.store.findByIdOrName(ownerId, idOrName);
111265
111633
  if (!descriptor) throw new HttpError("not_found", `Datalake not found: ${idOrName}`);
111266
111634
  return descriptor;
111267
111635
  }
111268
111636
  var ADDRESS_PATTERN = /^(0x)?[\da-fA-F]{40}$/;
111269
- function normalizePrincipal(value) {
111637
+ function normalizePrincipal$1(value) {
111270
111638
  if (isPublicPrincipal(value)) return value;
111271
111639
  if (ADDRESS_PATTERN.test(value)) return normalizeAddress(value);
111272
111640
  return value;
111273
111641
  }
111274
111642
  async function buildServer(options) {
111275
- const app = (0, import_fastify.default)({ logger: options.logger ?? false });
111643
+ const app = (0, import_fastify.default)({
111644
+ logger: options.logger ?? false,
111645
+ logController: new import_fastify.LogController({ disableRequestLogging: true })
111646
+ });
111647
+ registerExactCors(app, {
111648
+ allowedOrigins: options.corsOrigins ?? [],
111649
+ exposedHeaders: [
111650
+ DATALAKE_HEADER_DUPLICATES,
111651
+ DATALAKE_HEADER_REJECTED,
111652
+ DATALAKE_HEADER_NEXT_CURSOR,
111653
+ "x-ratelimit-limit",
111654
+ "x-ratelimit-remaining",
111655
+ "retry-after"
111656
+ ]
111657
+ });
111276
111658
  registerErrorHandler(app);
111277
111659
  registerAuth(app, options.tokenStore, [
111278
111660
  datalakeHealthPath(),
@@ -111498,29 +111880,253 @@ function escapeRegex(input) {
111498
111880
  return input.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
111499
111881
  }
111500
111882
  //#endregion
111883
+ //#region src/errors/planeErrors.ts
111884
+ /**
111885
+ * Thrown inside data-plane handlers to produce a structured JSON error.
111886
+ * Distinct from the control-plane `HttpError` only for type hygiene; the
111887
+ * wire format is identical.
111888
+ */
111889
+ var PlaneHttpError = class extends Error {
111890
+ code;
111891
+ statusCode;
111892
+ constructor(code, message) {
111893
+ super(message);
111894
+ this.name = "PlaneHttpError";
111895
+ this.code = code;
111896
+ this.statusCode = API_ERROR_STATUS[code];
111897
+ }
111898
+ };
111899
+ function toPlaneErrorBody(error, requestId) {
111900
+ return {
111901
+ code: error.code,
111902
+ message: error.message,
111903
+ requestId
111904
+ };
111905
+ }
111906
+ //#endregion
111907
+ //#region src/auth/accessContext.ts
111908
+ const VIEWER_OPERATIONS = ["payload:read", "usage:read"];
111909
+ const RUNNER_OPERATIONS = [
111910
+ ...DATALAKE_DATA_OPERATIONS,
111911
+ "payload:clear",
111912
+ "payload:delete"
111913
+ ];
111914
+ /** Resolves and attaches access once, before quota and route authorization. */
111915
+ function registerAccessContext(app, options) {
111916
+ app.addHook("preHandler", async (request) => {
111917
+ const datalakeId = extractDatalakeId$2(request);
111918
+ if (datalakeId === void 0) return;
111919
+ request.datalakeAccess = await resolveAccess({
111920
+ allowedOrigins: options.allowedOrigins,
111921
+ authorizationHeader: request.headers.authorization,
111922
+ controlStore: options.controlStore,
111923
+ datalakeId,
111924
+ origin: request.headers.origin,
111925
+ isServiceWalletJwtAllowed: options.isServiceWalletJwtAllowed,
111926
+ signingSecret: options.signingSecret
111927
+ });
111928
+ });
111929
+ }
111930
+ /**
111931
+ * Non-throwing access resolution keeps invalid credentials available to the
111932
+ * later anonymous-IP rate limiter. Routes call `requireAccessContext` to
111933
+ * surface the stored error and enforce their operation without re-verifying.
111934
+ */
111935
+ async function resolveAccess(input) {
111936
+ const descriptor = await input.controlStore.findById(input.datalakeId);
111937
+ if (!descriptor) return failure(false, new PlaneHttpError("not_found", `Datalake not found: ${input.datalakeId}`));
111938
+ const tokenValue = extractBearer(input.authorizationHeader);
111939
+ if (tokenValue !== void 0) {
111940
+ const verified = await verifyBearer(tokenValue, descriptor, input);
111941
+ if (!verified.ok) return {
111942
+ authenticated: false,
111943
+ descriptor,
111944
+ error: new PlaneHttpError("unauthorized", verified.message)
111945
+ };
111946
+ const maxRole = findLiveRole(descriptor, verified.principal);
111947
+ if (maxRole === void 0) return {
111948
+ authenticated: true,
111949
+ descriptor,
111950
+ principal: verified.principal,
111951
+ error: new PlaneHttpError("forbidden", "Verified principal has no live access to this datalake")
111952
+ };
111953
+ const effectiveOperations = intersectOperations(verified.operations, operationsForRole(maxRole));
111954
+ const context = {
111955
+ authenticated: true,
111956
+ descriptor,
111957
+ principal: verified.principal,
111958
+ maxRole,
111959
+ effectiveOperations
111960
+ };
111961
+ return {
111962
+ authenticated: true,
111963
+ descriptor,
111964
+ principal: verified.principal,
111965
+ context
111966
+ };
111967
+ }
111968
+ const publicRole = findPublicLiveRole(descriptor);
111969
+ if (publicRole !== void 0) return {
111970
+ authenticated: false,
111971
+ descriptor,
111972
+ context: {
111973
+ authenticated: false,
111974
+ descriptor,
111975
+ maxRole: publicRole,
111976
+ effectiveOperations: operationsForRole(publicRole)
111977
+ }
111978
+ };
111979
+ return {
111980
+ authenticated: false,
111981
+ descriptor,
111982
+ error: new PlaneHttpError("unauthorized", "Authentication required")
111983
+ };
111984
+ }
111985
+ function requireAccessContext(request, operation) {
111986
+ const resolution = request.datalakeAccess;
111987
+ if (resolution?.error !== void 0) throw resolution.error;
111988
+ const context = resolution?.context;
111989
+ if (context === void 0) throw new PlaneHttpError("internal", "Datalake access context is missing");
111990
+ if (!context.effectiveOperations.includes(operation)) throw new PlaneHttpError("forbidden", `Operation ${operation} is not authorized`);
111991
+ return context;
111992
+ }
111993
+ async function verifyBearer(token, descriptor, input) {
111994
+ if (isEs256kJwt(token)) return verifyWalletBearer(token, descriptor, input);
111995
+ if (input.origin !== void 0) return {
111996
+ ok: false,
111997
+ message: "Browser requests require an origin-bound wallet JWT"
111998
+ };
111999
+ try {
112000
+ const payload = verifyToken({
112001
+ token,
112002
+ signingSecret: input.signingSecret
112003
+ });
112004
+ if (payload.dl !== descriptor.id) return {
112005
+ ok: false,
112006
+ message: "Token is not scoped to this datalake"
112007
+ };
112008
+ return {
112009
+ ok: true,
112010
+ principal: normalizePrincipal(payload.sub),
112011
+ operations: operationsForRole(payload.role)
112012
+ };
112013
+ } catch (error) {
112014
+ return {
112015
+ ok: false,
112016
+ message: error instanceof Error ? error.message : "Invalid token"
112017
+ };
112018
+ }
112019
+ }
112020
+ async function verifyWalletBearer(token, descriptor, input) {
112021
+ const isBrowser = input.origin !== void 0 || hasBrowserClaims(token);
112022
+ if (!isBrowser && !input.isServiceWalletJwtAllowed) return {
112023
+ ok: false,
112024
+ message: "Non-browser wallet JWT profile is not enabled"
112025
+ };
112026
+ const result = await verifyWalletJwtPolicy(token, {
112027
+ audience: descriptor.id,
112028
+ profile: isBrowser ? {
112029
+ kind: "browser",
112030
+ origin: input.origin,
112031
+ allowedOrigins: input.allowedOrigins,
112032
+ allowedScopes: DATALAKE_DATA_OPERATIONS
112033
+ } : { kind: "service" }
112034
+ });
112035
+ if (!result.ok) return {
112036
+ ok: false,
112037
+ message: `Wallet JWT verification failed: ${result.reasons.join("; ")}`
112038
+ };
112039
+ return {
112040
+ ok: true,
112041
+ principal: result.principal,
112042
+ operations: isBrowser ? result.scopes : RUNNER_OPERATIONS
112043
+ };
112044
+ }
112045
+ function findLiveRole(descriptor, principal) {
112046
+ if (normalizePrincipal(descriptor.ownerId) === principal) return "runner";
112047
+ let role;
112048
+ for (const candidate of descriptor.acl) {
112049
+ if (isPublicPrincipal(candidate.principal) || normalizePrincipal(candidate.principal) !== principal) continue;
112050
+ if (candidate.role === "runner") return "runner";
112051
+ role = "viewer";
112052
+ }
112053
+ return role;
112054
+ }
112055
+ function findPublicLiveRole(descriptor) {
112056
+ let role;
112057
+ for (const candidate of descriptor.acl) {
112058
+ if (!isPublicPrincipal(candidate.principal)) continue;
112059
+ if (candidate.role === "runner") return "runner";
112060
+ role = "viewer";
112061
+ }
112062
+ return role;
112063
+ }
112064
+ function operationsForRole(role) {
112065
+ return role === "runner" ? RUNNER_OPERATIONS : VIEWER_OPERATIONS;
112066
+ }
112067
+ function intersectOperations(tokenOperations, liveOperations) {
112068
+ const live = new Set(liveOperations);
112069
+ return tokenOperations.filter((operation) => live.has(operation));
112070
+ }
112071
+ function isEs256kJwt(token) {
112072
+ try {
112073
+ return decodeJwt(token).header.alg === JwtAlg.ES256K;
112074
+ } catch {
112075
+ return false;
112076
+ }
112077
+ }
112078
+ function hasBrowserClaims(token) {
112079
+ try {
112080
+ const payload = decodeJwt(token).payload;
112081
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload) && ("origin" in payload || "scope" in payload);
112082
+ } catch {
112083
+ return false;
112084
+ }
112085
+ }
112086
+ function extractBearer(authorization) {
112087
+ if (authorization === void 0 || !authorization.toLowerCase().startsWith("bearer ")) return void 0;
112088
+ const value = authorization.slice(7).trim();
112089
+ return value.length > 0 ? value : void 0;
112090
+ }
112091
+ function normalizePrincipal(value) {
112092
+ if (/^(?:0x)?[\da-fA-F]{40}$/.test(value)) return normalizeAddress(value);
112093
+ return value;
112094
+ }
112095
+ function extractDatalakeId$2(request) {
112096
+ const value = request.params?.id;
112097
+ return typeof value === "string" ? value : void 0;
112098
+ }
112099
+ function failure(authenticated, error) {
112100
+ return {
112101
+ authenticated,
112102
+ error
112103
+ };
112104
+ }
112105
+ //#endregion
111501
112106
  //#region src/plugins/auditPlugin.ts
111502
112107
  const START_TIME_KEY = "__ariesAuditStart__";
111503
112108
  /**
111504
112109
  * Installs onRequest + onResponse hooks that record one JSONL row per
111505
112110
  * request to the configured `AuditLogger`. Health checks are skipped.
111506
112111
  */
111507
- function registerAudit(app, logger) {
112112
+ function registerAudit(app, logger, principalHashSecret) {
111508
112113
  app.addHook("onRequest", async (request) => {
111509
112114
  request[START_TIME_KEY] = Date.now();
111510
112115
  });
111511
112116
  app.addHook("onResponse", async (request, reply) => {
111512
- const pathname = (request.raw.url ?? request.url).split("?", 1)[0] ?? "";
111513
- if (pathname.endsWith("/health")) return;
112117
+ const path = routeTemplate(request);
112118
+ if (path.endsWith("/health")) return;
111514
112119
  const start = request[START_TIME_KEY] ?? Date.now();
111515
112120
  const durationMs = Date.now() - start;
111516
- const { subject, authenticated } = resolveSubject$1(request);
112121
+ const { subject, authenticated } = resolveSubject$1(request, principalHashSecret);
111517
112122
  const datalakeId = extractDatalakeId$1(request);
112123
+ const datalakeSubject = datalakeId === void 0 ? void 0 : `datalake:${stableDatalakeHash(datalakeId, principalHashSecret)}`;
111518
112124
  logger.log({
111519
112125
  time: (/* @__PURE__ */ new Date()).toISOString(),
111520
112126
  requestId: request.id,
111521
112127
  method: request.method,
111522
- path: pathname,
111523
- datalakeId,
112128
+ path,
112129
+ datalakeSubject,
111524
112130
  subject,
111525
112131
  authenticated,
111526
112132
  statusCode: reply.statusCode,
@@ -111529,49 +112135,33 @@ function registerAudit(app, logger) {
111529
112135
  });
111530
112136
  });
111531
112137
  }
111532
- function resolveSubject$1(request) {
111533
- const authorization = request.headers.authorization;
111534
- if (authorization?.toLowerCase().startsWith("bearer ")) {
111535
- const token = authorization.slice(7).trim();
111536
- return {
111537
- subject: `auth:${createHash("sha256").update(token).digest("hex").slice(0, 16)}`,
111538
- authenticated: true
111539
- };
111540
- }
112138
+ function resolveSubject$1(request, principalHashSecret) {
112139
+ const principal = request.datalakeAccess?.principal;
112140
+ if (principal !== void 0) return {
112141
+ subject: `principal:${stablePrincipalHash(principal, principalHashSecret)}`,
112142
+ authenticated: true
112143
+ };
111541
112144
  return {
111542
112145
  subject: `anonymous:${request.ip}`,
111543
112146
  authenticated: false
111544
112147
  };
111545
112148
  }
112149
+ function stablePrincipalHash(principal, secret) {
112150
+ return createHmac("sha256", secret).update("aries-datalake-principal-v1\0").update(principal).digest("hex").slice(0, 32);
112151
+ }
112152
+ function stableDatalakeHash(datalakeId, secret) {
112153
+ return createHmac("sha256", secret).update("aries-datalake-id-v1\0").update(datalakeId).digest("hex").slice(0, 32);
112154
+ }
112155
+ function routeTemplate(request) {
112156
+ const route = request.routeOptions?.url;
112157
+ if (typeof route !== "string" || route.length === 0) return "unmatched";
112158
+ return route.split("?", 1)[0] ?? "unmatched";
112159
+ }
111546
112160
  function extractDatalakeId$1(request) {
111547
112161
  const value = request.params?.id;
111548
112162
  return typeof value === "string" ? value : void 0;
111549
112163
  }
111550
112164
  //#endregion
111551
- //#region src/errors/planeErrors.ts
111552
- /**
111553
- * Thrown inside data-plane handlers to produce a structured JSON error.
111554
- * Distinct from the control-plane `HttpError` only for type hygiene; the
111555
- * wire format is identical.
111556
- */
111557
- var PlaneHttpError = class extends Error {
111558
- code;
111559
- statusCode;
111560
- constructor(code, message) {
111561
- super(message);
111562
- this.name = "PlaneHttpError";
111563
- this.code = code;
111564
- this.statusCode = API_ERROR_STATUS$1[code];
111565
- }
111566
- };
111567
- function toPlaneErrorBody(error, requestId) {
111568
- return {
111569
- code: error.code,
111570
- message: error.message,
111571
- requestId
111572
- };
111573
- }
111574
- //#endregion
111575
112165
  //#region src/plugins/errorHandler.ts
111576
112166
  function registerPlaneErrorHandler(app) {
111577
112167
  app.setErrorHandler((error, request, reply) => {
@@ -111695,7 +112285,7 @@ function registerMetrics(app) {
111695
112285
  registers: [registry]
111696
112286
  });
111697
112287
  app.addHook("onResponse", (request, reply, done) => {
111698
- const route = (request.routeOptions?.url ?? request.url ?? "unknown").split("?", 1)[0] ?? "unknown";
112288
+ const route = request.routeOptions?.url?.split("?", 1)[0] ?? "unmatched";
111699
112289
  const labels = {
111700
112290
  method: request.method,
111701
112291
  route,
@@ -111718,17 +112308,15 @@ function registerMetrics(app) {
111718
112308
  * request. Fires after route matching, so the health endpoint (no `:id`
111719
112309
  * param) is not rate limited.
111720
112310
  *
111721
- * For keying purposes the plugin peeks at the Authorization header but does
111722
- * not verify it verification is the route handler's job. The keying is
111723
- * tolerant of forged tokens because an unverified attacker still ends up
111724
- * in their own bucket (and 401s at the route handler).
112311
+ * Access resolution runs first. Verified tokens key by stable principal;
112312
+ * missing or invalid credentials consume the anonymous IP bucket.
111725
112313
  */
111726
112314
  function registerRateLimit(app, deps) {
111727
112315
  const config = "check" in deps ? { limiter: deps } : deps;
111728
112316
  app.addHook("preHandler", async (request, reply) => {
111729
112317
  const datalakeId = extractDatalakeId(request);
111730
112318
  if (!datalakeId) return;
111731
- const override = await resolveOverride(config.controlStore, datalakeId);
112319
+ const override = resolveOverride(request);
111732
112320
  const { subject, authenticated } = resolveSubject(request);
111733
112321
  const decision = config.limiter.check({
111734
112322
  datalakeId,
@@ -111748,22 +112336,18 @@ function extractDatalakeId(request) {
111748
112336
  return typeof value === "string" ? value : void 0;
111749
112337
  }
111750
112338
  function resolveSubject(request) {
111751
- const authorization = request.headers.authorization;
111752
- if (authorization?.toLowerCase().startsWith("bearer ")) {
111753
- const token = authorization.slice(7).trim();
111754
- return {
111755
- subject: `auth:${createHash("sha256").update(token).digest("hex").slice(0, 16)}`,
111756
- authenticated: true
111757
- };
111758
- }
112339
+ const principal = request.datalakeAccess?.principal;
112340
+ if (principal !== void 0) return {
112341
+ subject: `auth:${principal}`,
112342
+ authenticated: true
112343
+ };
111759
112344
  return {
111760
112345
  subject: `anonymous:${request.ip}`,
111761
112346
  authenticated: false
111762
112347
  };
111763
112348
  }
111764
- async function resolveOverride(store, datalakeId) {
111765
- if (!store) return void 0;
111766
- return (await store.findById(datalakeId))?.config.rateLimits;
112349
+ function resolveOverride(request) {
112350
+ return request.datalakeAccess?.descriptor?.config.rateLimits;
111767
112351
  }
111768
112352
  //#endregion
111769
112353
  //#region src/rateLimit/TokenBucketRateLimiter.ts
@@ -111859,119 +112443,11 @@ function registerPlaneHealthRoute(app, version) {
111859
112443
  });
111860
112444
  }
111861
112445
  //#endregion
111862
- //#region src/auth/accessContext.ts
111863
- const ROLE_RANK = {
111864
- viewer: 1,
111865
- runner: 2
111866
- };
111867
- /**
111868
- * Resolves whether a request may proceed. Order of precedence:
111869
- *
111870
- * 1. A valid bearer token scoped to this datalake → authenticated, uses the
111871
- * role encoded in the JWT.
111872
- * 2. No token but the datalake has a `public` ACL grant with at least the
111873
- * required role → unauthenticated public access.
111874
- *
111875
- * Any other state is an error: 401 for bad/missing creds on a private
111876
- * datalake, 403 for insufficient role.
111877
- */
111878
- async function resolveAccess(input) {
111879
- const descriptor = await input.controlStore.findById(input.datalakeId);
111880
- if (!descriptor) throw new PlaneHttpError("not_found", `Datalake not found: ${input.datalakeId}`);
111881
- const tokenValue = extractBearer(input.authorizationHeader);
111882
- if (tokenValue) {
111883
- if (isWalletJwt(tokenValue)) {
111884
- const result = await resolveWalletJwt(tokenValue, descriptor);
111885
- ensureRole(result.role, input.requiredRole);
111886
- return {
111887
- descriptor,
111888
- role: result.role,
111889
- authenticated: true,
111890
- token: result.token
111891
- };
111892
- }
111893
- const payload = verifyBearer(tokenValue, input.signingSecret, descriptor.id);
111894
- ensureRole(payload.role, input.requiredRole);
111895
- return {
111896
- descriptor,
111897
- role: payload.role,
111898
- authenticated: true,
111899
- token: payload
111900
- };
111901
- }
111902
- const publicGrant = descriptor.acl.find((entry) => isPublicPrincipal(entry.principal));
111903
- if (publicGrant) {
111904
- ensureRole(publicGrant.role, input.requiredRole);
111905
- return {
111906
- descriptor,
111907
- role: publicGrant.role,
111908
- authenticated: false
111909
- };
111910
- }
111911
- throw new PlaneHttpError("unauthorized", "Authentication required");
111912
- }
111913
- function extractBearer(authorization) {
111914
- if (!authorization) return void 0;
111915
- if (!authorization.toLowerCase().startsWith("bearer ")) return void 0;
111916
- return authorization.slice(7).trim();
111917
- }
111918
- function verifyBearer(token, signingSecret, datalakeId) {
111919
- try {
111920
- const payload = verifyToken({
111921
- token,
111922
- signingSecret
111923
- });
111924
- if (payload.dl !== datalakeId) throw new PlaneHttpError("forbidden", "Token is not scoped to this datalake");
111925
- return payload;
111926
- } catch (error) {
111927
- if (error instanceof PlaneHttpError) throw error;
111928
- throw new PlaneHttpError("unauthorized", error instanceof Error ? error.message : "Invalid token");
111929
- }
111930
- }
111931
- function ensureRole(granted, required) {
111932
- if (ROLE_RANK[granted] < ROLE_RANK[required]) throw new PlaneHttpError("forbidden", `Role ${granted} is insufficient; ${required} required`);
111933
- }
111934
- function isWalletJwt(token) {
111935
- try {
111936
- return decodeJwt(token).header.alg === JwtAlg.ES256K;
111937
- } catch {
111938
- return false;
111939
- }
111940
- }
111941
- async function resolveWalletJwt(token, descriptor) {
111942
- const result = await verifyJwt(token, { audience: descriptor.id });
111943
- if (!result.ok) throw new PlaneHttpError("unauthorized", `Wallet JWT verification failed: ${result.reasons.join("; ")}`);
111944
- const callerAddress = normalizeAddress(result.payload.iss);
111945
- const ownerAddress = normalizeAddress(descriptor.ownerId);
111946
- let role;
111947
- if (callerAddress === ownerAddress) role = "runner";
111948
- else role = descriptor.acl.find((e) => !isPublicPrincipal(e.principal) && normalizeAddress(e.principal) === callerAddress)?.role;
111949
- if (!role) throw new PlaneHttpError("forbidden", `Address ${callerAddress} has no access to this datalake`);
111950
- const synthesized = {
111951
- sub: callerAddress,
111952
- dl: descriptor.id,
111953
- role,
111954
- iat: result.payload.iat,
111955
- exp: result.payload.exp,
111956
- jti: `wallet:${callerAddress}:${result.payload.iat}`
111957
- };
111958
- return {
111959
- role,
111960
- token: synthesized
111961
- };
111962
- }
111963
- //#endregion
111964
112446
  //#region src/routes/payloadRoutes.ts
111965
112447
  const PLANE_PREFIX$1 = `/v1/datalakes/:id`;
111966
112448
  function registerPayloadRoutes(app, deps) {
111967
112449
  app.post(`${PLANE_PREFIX$1}/insert`, async (request, reply) => {
111968
- const access = await resolveAccess({
111969
- controlStore: deps.controlStore,
111970
- signingSecret: deps.signingSecret,
111971
- datalakeId: request.params.id,
111972
- authorizationHeader: request.headers.authorization,
111973
- requiredRole: "runner"
111974
- });
112450
+ const access = requireAccessContext(request, "payload:append");
111975
112451
  const payloads = normalizeInsertBody(request.body);
111976
112452
  if (payloads.length === 0) {
111977
112453
  setInsertSummaryHeaders(reply, 0, []);
@@ -111984,38 +112460,20 @@ function registerPayloadRoutes(app, deps) {
111984
112460
  return result.inserted;
111985
112461
  });
111986
112462
  app.post(`${PLANE_PREFIX$1}/get`, async (request) => {
111987
- const access = await resolveAccess({
111988
- controlStore: deps.controlStore,
111989
- signingSecret: deps.signingSecret,
111990
- datalakeId: request.params.id,
111991
- authorizationHeader: request.headers.authorization,
111992
- requiredRole: "viewer"
111993
- });
112463
+ const access = requireAccessContext(request, "payload:read");
111994
112464
  const hashes = parseHashesBody(request.body);
111995
112465
  if (hashes.length === 0) throw new PlaneHttpError("bad_request", "Body must be a non-empty array of hashes");
111996
112466
  if (hashes.length > deps.maxPageSize) throw new PlaneHttpError("unprocessable", `Cannot request more than ${deps.maxPageSize} hashes per call`);
111997
112467
  return deps.payloadStore.get(access.descriptor.id, hashes);
111998
112468
  });
111999
112469
  app.get(`${PLANE_PREFIX$1}/get/:hash`, async (request) => {
112000
- const access = await resolveAccess({
112001
- controlStore: deps.controlStore,
112002
- signingSecret: deps.signingSecret,
112003
- datalakeId: request.params.id,
112004
- authorizationHeader: request.headers.authorization,
112005
- requiredRole: "viewer"
112006
- });
112470
+ const access = requireAccessContext(request, "payload:read");
112007
112471
  const [payload] = await deps.payloadStore.get(access.descriptor.id, [request.params.hash]);
112008
112472
  if (!payload) throw new PlaneHttpError("not_found", `Payload not found: ${request.params.hash}`);
112009
112473
  return payload;
112010
112474
  });
112011
112475
  app.get(`${PLANE_PREFIX$1}/next`, async (request, reply) => {
112012
- const access = await resolveAccess({
112013
- controlStore: deps.controlStore,
112014
- signingSecret: deps.signingSecret,
112015
- datalakeId: request.params.id,
112016
- authorizationHeader: request.headers.authorization,
112017
- requiredRole: "viewer"
112018
- });
112476
+ const access = requireAccessContext(request, "payload:read");
112019
112477
  const options = {
112020
112478
  limit: request.query.limit === void 0 ? void 0 : Number(request.query.limit),
112021
112479
  cursor: request.query.cursor,
@@ -112025,35 +112483,17 @@ function registerPayloadRoutes(app, deps) {
112025
112483
  return runNext(deps, access.descriptor.id, options, reply);
112026
112484
  });
112027
112485
  app.post(`${PLANE_PREFIX$1}/next`, async (request, reply) => {
112028
- const access = await resolveAccess({
112029
- controlStore: deps.controlStore,
112030
- signingSecret: deps.signingSecret,
112031
- datalakeId: request.params.id,
112032
- authorizationHeader: request.headers.authorization,
112033
- requiredRole: "viewer"
112034
- });
112486
+ const access = requireAccessContext(request, "payload:read");
112035
112487
  const options = request.body ?? {};
112036
112488
  return runNext(deps, access.descriptor.id, options, reply);
112037
112489
  });
112038
112490
  app.delete(`${PLANE_PREFIX$1}/delete/:hash`, async (request, reply) => {
112039
- const access = await resolveAccess({
112040
- controlStore: deps.controlStore,
112041
- signingSecret: deps.signingSecret,
112042
- datalakeId: request.params.id,
112043
- authorizationHeader: request.headers.authorization,
112044
- requiredRole: "runner"
112045
- });
112491
+ const access = requireAccessContext(request, "payload:delete");
112046
112492
  if (!await deps.payloadStore.delete(access.descriptor.id, request.params.hash)) throw new PlaneHttpError("not_found", `Payload not found: ${request.params.hash}`);
112047
112493
  reply.code(204).send();
112048
112494
  });
112049
112495
  app.post(`${PLANE_PREFIX$1}/clear`, async (request) => {
112050
- const access = await resolveAccess({
112051
- controlStore: deps.controlStore,
112052
- signingSecret: deps.signingSecret,
112053
- datalakeId: request.params.id,
112054
- authorizationHeader: request.headers.authorization,
112055
- requiredRole: "runner"
112056
- });
112496
+ const access = requireAccessContext(request, "payload:clear");
112057
112497
  return { removed: await deps.payloadStore.clear(access.descriptor.id) };
112058
112498
  });
112059
112499
  }
@@ -112127,13 +112567,7 @@ const PLANE_PREFIX = `/v1/datalakes/:id`;
112127
112567
  */
112128
112568
  function registerUsageRoute(app, deps) {
112129
112569
  app.get(`${PLANE_PREFIX}/usage`, async (request) => {
112130
- const access = await resolveAccess({
112131
- controlStore: deps.controlStore,
112132
- signingSecret: deps.signingSecret,
112133
- datalakeId: request.params.id,
112134
- authorizationHeader: request.headers.authorization,
112135
- requiredRole: "viewer"
112136
- });
112570
+ const access = requireAccessContext(request, "usage:read");
112137
112571
  const descriptor = access.descriptor;
112138
112572
  const payloadCount = await deps.payloadStore.count(descriptor.id);
112139
112573
  const response = {
@@ -112142,10 +112576,10 @@ function registerUsageRoute(app, deps) {
112142
112576
  };
112143
112577
  if (deps.rateLimiter) {
112144
112578
  const override = descriptor.config.rateLimits;
112145
- const authSubject = access.token?.sub ?? descriptor.ownerId;
112579
+ const authSubject = access.principal ?? descriptor.ownerId;
112146
112580
  const authPeek = deps.rateLimiter.peek({
112147
112581
  datalakeId: descriptor.id,
112148
- subject: `auth-peek:${authSubject}`
112582
+ subject: `auth:${authSubject}`
112149
112583
  }, true, override);
112150
112584
  const anonPeek = deps.rateLimiter.peek({
112151
112585
  datalakeId: descriptor.id,
@@ -112173,11 +112607,29 @@ function registerUsageRoute(app, deps) {
112173
112607
  */
112174
112608
  async function buildPlaneServer(options) {
112175
112609
  const app = (0, import_fastify.default)({
112610
+ bodyLimit: 10 * 1024 * 1024,
112176
112611
  logger: options.logger ?? false,
112177
- bodyLimit: 10 * 1024 * 1024
112612
+ logController: new import_fastify.LogController({ disableRequestLogging: true })
112613
+ });
112614
+ registerExactCors(app, {
112615
+ allowedOrigins: options.corsOrigins ?? [],
112616
+ exposedHeaders: [
112617
+ DATALAKE_HEADER_DUPLICATES,
112618
+ DATALAKE_HEADER_REJECTED,
112619
+ DATALAKE_HEADER_NEXT_CURSOR,
112620
+ "x-ratelimit-limit",
112621
+ "x-ratelimit-remaining",
112622
+ "retry-after"
112623
+ ]
112178
112624
  });
112179
112625
  registerGzipJsonParser(app);
112180
112626
  registerPlaneErrorHandler(app);
112627
+ registerAccessContext(app, {
112628
+ allowedOrigins: options.corsOrigins ?? [],
112629
+ controlStore: options.controlStore,
112630
+ isServiceWalletJwtAllowed: options.isServiceWalletJwtAllowed ?? false,
112631
+ signingSecret: options.signingSecret
112632
+ });
112181
112633
  let limiter;
112182
112634
  if (options.rateLimiter !== false) {
112183
112635
  limiter = options.rateLimiter ?? new TokenBucketRateLimiter({
@@ -112185,26 +112637,19 @@ async function buildPlaneServer(options) {
112185
112637
  anonymousPerMinute: 20,
112186
112638
  burstFactor: 2
112187
112639
  });
112188
- registerRateLimit(app, {
112189
- limiter,
112190
- controlStore: options.controlStore
112191
- });
112640
+ registerRateLimit(app, { limiter });
112192
112641
  }
112193
- registerAudit(app, options.auditLogger ?? new NullAuditLogger());
112642
+ registerAudit(app, options.auditLogger ?? new NullAuditLogger(), options.auditPrincipalSecret ?? options.signingSecret);
112194
112643
  registerMetrics(app);
112195
112644
  registerPlaneHealthRoute(app, options.version);
112196
112645
  const maxInsertBatch = options.maxInsertBatch ?? 1e3;
112197
112646
  registerPayloadRoutes(app, {
112198
- controlStore: options.controlStore,
112199
112647
  payloadStore: options.payloadStore,
112200
- signingSecret: options.signingSecret,
112201
112648
  maxInsertBatch,
112202
112649
  maxPageSize: options.maxPageSize ?? 1e3
112203
112650
  });
112204
112651
  registerUsageRoute(app, {
112205
- controlStore: options.controlStore,
112206
112652
  payloadStore: options.payloadStore,
112207
- signingSecret: options.signingSecret,
112208
112653
  rateLimiter: limiter
112209
112654
  });
112210
112655
  return app;
@@ -112401,6 +112846,7 @@ const PERSIST_DIR = process.env.DATALAKE_PERSIST_DIR;
112401
112846
  const AUDIT_LOG = process.env.DATALAKE_AUDIT_LOG;
112402
112847
  const AUDIT_DIR = process.env.DATALAKE_AUDIT_DIR;
112403
112848
  const AUDIT_MAX_AGE = process.env.DATALAKE_AUDIT_MAX_AGE_DAYS;
112849
+ const CORS_ORIGINS = parseCorsOrigins(process.env.DATALAKE_CORS_ORIGINS);
112404
112850
  function makeStores() {
112405
112851
  if (!PERSIST_DIR) return {
112406
112852
  controlStore: new InMemoryDatalakeStore(),
@@ -112414,7 +112860,13 @@ function makeStores() {
112414
112860
  };
112415
112861
  }
112416
112862
  function makeTokenStore() {
112417
- const stores = [new SelfSignedWalletJwtTokenStore(DATALAKE_CONTROL_AUDIENCE), new WalletJwtTokenStore(SIGNING_SECRET)];
112863
+ const stores = [new SelfSignedWalletJwtTokenStore(DATALAKE_CONTROL_AUDIENCE, {
112864
+ browser: {
112865
+ allowedOrigins: CORS_ORIGINS,
112866
+ allowedScopes: DATALAKE_CONTROL_OPERATIONS
112867
+ },
112868
+ isServiceProfileAllowed: true
112869
+ }), new WalletJwtTokenStore(SIGNING_SECRET)];
112418
112870
  const labels = [`self-signed-jwt(aud=${DATALAKE_CONTROL_AUDIENCE})`, "session-jwt"];
112419
112871
  const staticTokens = process.env.DATALAKE_CONTROL_TOKENS;
112420
112872
  if (staticTokens && staticTokens.length > 0) {
@@ -112456,7 +112908,8 @@ async function main() {
112456
112908
  datalakeOriginUrl: DATALAKE_ORIGIN_URL,
112457
112909
  tokenSigningSecret: SIGNING_SECRET,
112458
112910
  version: VERSION,
112459
- logger: { level: process.env.LOG_LEVEL ?? "info" }
112911
+ logger: { level: process.env.LOG_LEVEL ?? "info" },
112912
+ corsOrigins: CORS_ORIGINS
112460
112913
  });
112461
112914
  const { logger: auditLogger, description: auditDescription } = makeAuditLogger();
112462
112915
  const plane = await buildPlaneServer({
@@ -112465,7 +112918,9 @@ async function main() {
112465
112918
  signingSecret: SIGNING_SECRET,
112466
112919
  version: VERSION,
112467
112920
  logger: { level: process.env.LOG_LEVEL ?? "info" },
112468
- auditLogger
112921
+ auditLogger,
112922
+ corsOrigins: CORS_ORIGINS,
112923
+ isServiceWalletJwtAllowed: true
112469
112924
  });
112470
112925
  await control.listen({
112471
112926
  port: CONTROL_PORT,