@tinycloud/node-sdk 3.4.0 → 3.5.0-beta.1

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/index.cjs CHANGED
@@ -17045,7 +17045,7 @@ function augmentSignInEntriesWithReplication(input) {
17045
17045
  for (const prefix of replication.prefixes) {
17046
17046
  (0, import_prefix.assertFolderPrefix)(prefix);
17047
17047
  const path = prefix === "/" ? "" : prefix;
17048
- if ((0, import_sdk_services.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17048
+ if ((0, import_sdk_services2.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17049
17049
  continue;
17050
17050
  }
17051
17051
  if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, path)) {
@@ -17071,12 +17071,12 @@ function augmentSignInEntriesWithReplication(input) {
17071
17071
  }
17072
17072
  return out;
17073
17073
  }
17074
- var import_sdk_core2, import_sdk_services, import_prefix, KV_GET, KV_SYNC, KV_SERVICE;
17074
+ var import_sdk_core2, import_sdk_services2, import_prefix, KV_GET, KV_SYNC, KV_SERVICE;
17075
17075
  var init_authority_sign_in = __esm({
17076
17076
  "src/replication/authority-sign-in.ts"() {
17077
17077
  "use strict";
17078
17078
  import_sdk_core2 = require("@tinycloud/sdk-core");
17079
- import_sdk_services = require("@tinycloud/sdk-services");
17079
+ import_sdk_services2 = require("@tinycloud/sdk-services");
17080
17080
  import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
17081
17081
  KV_GET = import_sdk_core2.KV.GET;
17082
17082
  KV_SYNC = import_sdk_core2.KV.SYNC;
@@ -18069,6 +18069,167 @@ var PrivateKeySigner = class {
18069
18069
  }
18070
18070
  };
18071
18071
 
18072
+ // src/recordingServiceContext.ts
18073
+ var import_sdk_services = require("@tinycloud/sdk-services");
18074
+ var import_requestLog = require("@tinycloud/sdk-services/telemetry/requestLog");
18075
+
18076
+ // src/recordingRegistry.ts
18077
+ var unrecordedFetches = /* @__PURE__ */ new WeakMap();
18078
+ function registerRecording(context, unrecordedFetch) {
18079
+ unrecordedFetches.set(context, unrecordedFetch);
18080
+ }
18081
+ function unrecordedFetchOf(context) {
18082
+ const unrecordedFetch = unrecordedFetches.get(context);
18083
+ return unrecordedFetch ? unrecordedFetch() : context.fetch;
18084
+ }
18085
+
18086
+ // src/recordingServiceContext.ts
18087
+ var SYNC_ABILITY = "tinycloud.kv/sync";
18088
+ var INFO_PATH = "/info";
18089
+ function outcomeCodeOf(response) {
18090
+ return response === void 0 ? "ERROR" : response.ok ? "ok" : `HTTP_${response.status}`;
18091
+ }
18092
+ function replicationOp(url, meta) {
18093
+ if (meta !== void 0) return meta.action === SYNC_ABILITY ? `${meta.service}/sync` : void 0;
18094
+ return isInfoUrl(url) ? "sync/info" : void 0;
18095
+ }
18096
+ function isInfoUrl(url) {
18097
+ try {
18098
+ return new URL(url).pathname.replace(/\/+$/, "") === INFO_PATH;
18099
+ } catch {
18100
+ return false;
18101
+ }
18102
+ }
18103
+ function replicationEvent(sample, op) {
18104
+ return (0, import_requestLog.buildRequestEvent)({
18105
+ service: "sync",
18106
+ op,
18107
+ url: sample.url,
18108
+ bytes: (0, import_requestLog.requestBytesOf)(sample.init?.body, sample.response?.headers.get("content-length")),
18109
+ durationMs: performance.now() - sample.clock,
18110
+ attempts: sample.attempts ?? 1,
18111
+ ok: sample.response?.ok === true,
18112
+ code: outcomeCodeOf(sample.response),
18113
+ at: sample.at
18114
+ });
18115
+ }
18116
+ function createServiceContext(config) {
18117
+ const { requestSink, ...base2 } = config;
18118
+ return requestSink === void 0 ? new import_sdk_services.ServiceContext(base2) : new RecordingServiceContext(base2, requestSink);
18119
+ }
18120
+ var RecordingServiceContext = class extends import_sdk_services.ServiceContext {
18121
+ constructor(config, sink) {
18122
+ const pending = {};
18123
+ super({
18124
+ ...config,
18125
+ invoke: rememberInvoke(config.invoke, sink, pending),
18126
+ invokeAny: config.invokeAny ? rememberInvokeAny(config.invokeAny, sink, pending) : void 0
18127
+ });
18128
+ this.requestSink = sink;
18129
+ this.pending = pending;
18130
+ registerRecording(this, () => this.unrecordedFetch);
18131
+ }
18132
+ /** Per-call recording; `_fetch` is read at access time, so overrides apply. */
18133
+ get fetch() {
18134
+ const raw = super.fetch;
18135
+ return (url, init2) => this.recordCall(raw, url, init2);
18136
+ }
18137
+ /** Transport without per-call recording. */
18138
+ get unrecordedFetch() {
18139
+ return super.fetch;
18140
+ }
18141
+ /**
18142
+ * One event per logical request. The first resolved attempt's init is the logged request and
18143
+ * `attempts` counts resolved builds. A retired context throws before the first build and records
18144
+ * nothing, as master does.
18145
+ */
18146
+ async invokeWithRetry(url, build) {
18147
+ const at = Date.now();
18148
+ const clock = performance.now();
18149
+ let reached = false;
18150
+ let attempts = 0;
18151
+ let first;
18152
+ let firstMeta;
18153
+ let response;
18154
+ const attempt = async () => {
18155
+ reached = true;
18156
+ this.pending.meta = void 0;
18157
+ const built = build();
18158
+ const meta = this.pending.meta;
18159
+ const init2 = await built;
18160
+ attempts += 1;
18161
+ if (first === void 0) {
18162
+ first = init2;
18163
+ firstMeta = meta;
18164
+ }
18165
+ return init2;
18166
+ };
18167
+ try {
18168
+ response = await super.invokeWithRetry(url, attempt);
18169
+ return response;
18170
+ } finally {
18171
+ if (reached) {
18172
+ this.observeAttempt({ url, init: first, response, meta: firstMeta, at, clock, attempts });
18173
+ }
18174
+ }
18175
+ }
18176
+ async recordCall(fetchFn, url, init2) {
18177
+ const at = Date.now();
18178
+ const clock = performance.now();
18179
+ let response;
18180
+ try {
18181
+ response = await fetchFn(url, init2);
18182
+ return response;
18183
+ } finally {
18184
+ this.observeAttempt({ url, init: init2, response, at, clock });
18185
+ }
18186
+ }
18187
+ observeAttempt(sample) {
18188
+ try {
18189
+ const replication = replicationOp(sample.url, sample.meta ?? (0, import_requestLog.invocationMetaOf)(sample.init?.headers));
18190
+ if (replication !== void 0) {
18191
+ this.requestSink.record(replicationEvent(sample, replication));
18192
+ return;
18193
+ }
18194
+ const headers = sample.init?.headers;
18195
+ if (sample.meta !== void 0 && typeof headers === "object" && headers !== null) {
18196
+ this.requestSink.remember(headers, sample.meta);
18197
+ }
18198
+ this.requestSink.observe({
18199
+ url: sample.url,
18200
+ requestHeaders: headers,
18201
+ requestBody: sample.init?.body,
18202
+ responseContentLength: sample.response?.headers.get("content-length"),
18203
+ ok: sample.response?.ok === true,
18204
+ code: outcomeCodeOf(sample.response),
18205
+ durationMs: performance.now() - sample.clock,
18206
+ attempts: sample.attempts ?? 1,
18207
+ at: sample.at
18208
+ });
18209
+ } catch {
18210
+ }
18211
+ }
18212
+ };
18213
+ function rememberInvoke(invoke2, sink, pending) {
18214
+ return (session, service, path, action, facts) => {
18215
+ const headers = invoke2(session, service, path, action, facts);
18216
+ const meta = { service, path, action, spaceId: session.spaceId };
18217
+ pending.meta = meta;
18218
+ sink.remember(headers, meta);
18219
+ return headers;
18220
+ };
18221
+ }
18222
+ function rememberInvokeAny(invokeAny2, sink, pending) {
18223
+ return (session, entries, facts) => {
18224
+ const headers = invokeAny2(session, entries, facts);
18225
+ const first = entries[0];
18226
+ const meta = first === void 0 ? void 0 : { service: first.service, path: first.path, action: first.action, spaceId: first.spaceId ?? session.spaceId };
18227
+ pending.meta = meta;
18228
+ if (meta !== void 0) sink.remember(headers, meta);
18229
+ return headers;
18230
+ };
18231
+ }
18232
+
18072
18233
  // src/delegation.ts
18073
18234
  var import_sdk_core = require("@tinycloud/sdk-core");
18074
18235
  var RUNTIME_DELEGATION_REJECTIONS = [
@@ -18625,7 +18786,8 @@ function getNodeReplicationLoaders() {
18625
18786
 
18626
18787
  // src/TinyCloudNode.ts
18627
18788
  var import_sdk_core9 = require("@tinycloud/sdk-core");
18628
- var import_sdk_services2 = require("@tinycloud/sdk-services");
18789
+ var import_sdk_services3 = require("@tinycloud/sdk-services");
18790
+ var import_requestLog2 = require("@tinycloud/sdk-services/telemetry/requestLog");
18629
18791
 
18630
18792
  // src/authorization/NodeUserAuthorization.ts
18631
18793
  var import_node_crypto = require("crypto");
@@ -20448,12 +20610,13 @@ var DelegatedAccess = class {
20448
20610
  this.session = session;
20449
20611
  this._delegation = delegation;
20450
20612
  this.host = host;
20451
- const context = new import_sdk_core6.ServiceContext({
20613
+ const context = createServiceContext({
20452
20614
  invoke: invoke2,
20453
20615
  invokeAny: invokeAny2,
20454
20616
  fetch: lifetime?.fetch ?? globalThis.fetch.bind(globalThis),
20455
20617
  hosts: [host],
20456
- telemetry
20618
+ telemetry,
20619
+ requestSink: lifetime?.requestSink
20457
20620
  });
20458
20621
  this._serviceContext = lifetime ? lifetime.track(context) : context;
20459
20622
  const prefix = this._delegation.path.replace(/\/$/, "");
@@ -20648,6 +20811,16 @@ function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
20648
20811
  return entries;
20649
20812
  }
20650
20813
  var DEFAULT_DELEGATION_EXPIRY_MS = import_sdk_core7.EXPIRY.SESSION_MS;
20814
+ var SHARE_NETWORK_NAME_RE = /^share-[a-z0-9][a-z0-9-]{15,35}$/;
20815
+ function grantsCappedDecrypt(path, actions) {
20816
+ if (!actions.some((action) => (0, import_sdk_core7.actionContains)(action, import_sdk_core7.ENCRYPTION.DECRYPT))) {
20817
+ return false;
20818
+ }
20819
+ return !((0, import_sdk_core7.isNetworkId)(path) && SHARE_NETWORK_NAME_RE.test((0, import_sdk_core7.parseNetworkId)(path).name));
20820
+ }
20821
+ function includesDecrypt(entries) {
20822
+ return entries.some((entry) => grantsCappedDecrypt(entry.path, entry.actions));
20823
+ }
20651
20824
  function resolveExpiryMs(expiry) {
20652
20825
  if (expiry === void 0) {
20653
20826
  return DEFAULT_DELEGATION_EXPIRY_MS;
@@ -20815,6 +20988,15 @@ var NodeSecretsService = class {
20815
20988
  if (this.hasPermission(permissionEntries)) {
20816
20989
  return ok();
20817
20990
  }
20991
+ let escalation;
20992
+ try {
20993
+ escalation = this.config.uncoveredEntries?.(permissionEntries) ?? permissionEntries;
20994
+ } catch (error) {
20995
+ return this.escalationError(action, target, error);
20996
+ }
20997
+ if (this.config.uncoveredEntries !== void 0 && escalation.length === 0) {
20998
+ return ok();
20999
+ }
20818
21000
  if (!this.config.canEscalate()) {
20819
21001
  return secretsError(
20820
21002
  import_sdk_core8.ErrorCodes.PERMISSION_DENIED,
@@ -20822,16 +21004,20 @@ var NodeSecretsService = class {
20822
21004
  );
20823
21005
  }
20824
21006
  try {
20825
- await this.config.grantPermissions(permissionEntries);
21007
+ const grant = action === "get" && this.config.grantGetPermissions !== void 0 ? this.config.grantGetPermissions : this.config.grantPermissions;
21008
+ await grant(escalation);
20826
21009
  return this.restoreUnlockAfterEscalation();
20827
21010
  } catch (error) {
20828
- return secretsError(
20829
- import_sdk_core8.ErrorCodes.PERMISSION_DENIED,
20830
- error instanceof Error ? error.message : `Autosign escalation for ${displayActionUrn(action)} on ${target} failed.`,
20831
- error instanceof Error ? error : void 0
20832
- );
21011
+ return this.escalationError(action, target, error);
20833
21012
  }
20834
21013
  }
21014
+ escalationError(action, target, error) {
21015
+ return secretsError(
21016
+ import_sdk_core8.ErrorCodes.PERMISSION_DENIED,
21017
+ error instanceof Error ? error.message : `Autosign escalation for ${displayActionUrn(action)} on ${target} failed.`,
21018
+ error instanceof Error ? error : void 0
21019
+ );
21020
+ }
20835
21021
  async restoreUnlockAfterEscalation() {
20836
21022
  if (!this.shouldRestoreUnlock) {
20837
21023
  return ok();
@@ -21043,11 +21229,12 @@ function createDefaultSessionKey(wasm) {
21043
21229
  return { manager, keyId, jwk: JSON.parse(jwkStr) };
21044
21230
  }
21045
21231
  var ServiceGraphLifetime = class {
21046
- constructor(invokeFn, invokeAnyFn, fetchFn, observeContext) {
21232
+ constructor(invokeFn, invokeAnyFn, fetchFn, observeContext, requestSink) {
21047
21233
  this.invokeFn = invokeFn;
21048
21234
  this.invokeAnyFn = invokeAnyFn;
21049
21235
  this.fetchFn = fetchFn;
21050
21236
  this.observeContext = observeContext;
21237
+ this.requestSink = requestSink;
21051
21238
  this.abortController = new AbortController();
21052
21239
  this.contexts = /* @__PURE__ */ new Set();
21053
21240
  this.retired = false;
@@ -21236,6 +21423,8 @@ function ownerPermissionsToAbilities(permissions) {
21236
21423
  return { abilities, rawAbilities };
21237
21424
  }
21238
21425
  var TERMINAL_DELEGATION_REQUIRES_SESSION_KEY = "disableSubDelegation requires a session-key delegation to a did:key (not did:pkh); use the session-key DID or drop the flag";
21426
+ var DECRYPT_GRANT_MAX_EXPIRY_MS = 60 * 60 * 1e3;
21427
+ var SECRET_GET_GRANT_EXPIRY_MS = 59 * 60 * 1e3;
21239
21428
  function base64UrlEncode(bytes) {
21240
21429
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
21241
21430
  let output = "";
@@ -21618,6 +21807,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21618
21807
  this.sessionManager = sessionKey.manager;
21619
21808
  this.sessionKeyId = sessionKey.keyId;
21620
21809
  this.sessionKeyJwk = sessionKey.jwk;
21810
+ this._requestLog = config.requestLog === true ? new import_requestLog2.RequestLog() : void 0;
21621
21811
  this._serviceGraph = this.createServiceGraphLifetime();
21622
21812
  this._capabilityRegistry = new import_sdk_core9.CapabilityKeyRegistry();
21623
21813
  this._keyProvider = new WasmKeyProvider({
@@ -21789,7 +21979,8 @@ var _TinyCloudNode = class _TinyCloudNode {
21789
21979
  createKVService: (config) => {
21790
21980
  const prefix = config.pathPrefix?.replace(/\/$/, "");
21791
21981
  const kvService = new import_sdk_core9.KVService({ prefix });
21792
- const kvContext = receiveOnlyGraph.track(new import_sdk_core9.ServiceContext({
21982
+ const kvContext = receiveOnlyGraph.track(createServiceContext({
21983
+ requestSink: receiveOnlyGraph.requestSink,
21793
21984
  invoke: config.invoke,
21794
21985
  fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
21795
21986
  hosts: config.hosts,
@@ -21852,6 +22043,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21852
22043
  shouldUseBootstrapSignInRequest(config) {
21853
22044
  return config.autoBootstrapAccount !== false && config.manifest === void 0 && config.capabilityRequest === void 0 && (config.prefix ?? "default") === "default" && isOpenKeyAutoSignStrategy(config.signStrategy);
21854
22045
  }
22046
+ /** Sanitized developer request events; undefined unless the node was built with `requestLog: true`. */
22047
+ get requests() {
22048
+ return this._requestLog === void 0 ? void 0 : (0, import_requestLog2.readOnlyRequestLog)(this._requestLog);
22049
+ }
21855
22050
  createServiceGraphLifetime() {
21856
22051
  return new ServiceGraphLifetime(
21857
22052
  this.invokeWithRuntimePermissions,
@@ -21862,7 +22057,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21862
22057
  (url, init2) => globalThis.fetch(url, init2),
21863
22058
  (context) => {
21864
22059
  this.storageFull.observe(context);
21865
- }
22060
+ if (this._requestLog) (0, import_requestLog2.observeReplicaReads)(context, this._requestLog);
22061
+ },
22062
+ this._requestLog
21866
22063
  );
21867
22064
  }
21868
22065
  syncResolvedHostFromAuth() {
@@ -22185,6 +22382,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22185
22382
  this.hostedOwnedSpaceIds.clear();
22186
22383
  this.cancelOwnedSpaceHosts();
22187
22384
  this.storageFull.clear();
22385
+ this._requestLog?.clear();
22188
22386
  this._bootstrapSkipped = false;
22189
22387
  this._bootstrapStatus = { skipped: false };
22190
22388
  this.replicationDelegateSession = void 0;
@@ -22276,7 +22474,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22276
22474
  const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
22277
22475
  this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22278
22476
  }
22279
- if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
22477
+ if (address !== this._address || chainId !== this._chainId) {
22478
+ this.storageFull.clear();
22479
+ this._requestLog?.clear();
22480
+ }
22280
22481
  this._address = address;
22281
22482
  this._chainId = chainId;
22282
22483
  this._kv = void 0;
@@ -22855,10 +23056,11 @@ var _TinyCloudNode = class _TinyCloudNode {
22855
23056
  const contexts = [context];
22856
23057
  const accountSpaceId = this.ownedSpaceId(import_sdk_core9.ACCOUNT_REGISTRY_SPACE);
22857
23058
  const createScopedContext = (spaceId) => {
22858
- const scoped = this._serviceGraph.track(new import_sdk_core9.ServiceContext({
23059
+ const scoped = this._serviceGraph.track(createServiceContext({
23060
+ requestSink: this._serviceGraph.requestSink,
22859
23061
  invoke: context.invoke,
22860
23062
  invokeAny: context.invokeAny,
22861
- fetch: context.fetch,
23063
+ fetch: unrecordedFetchOf(context),
22862
23064
  hosts: context.hosts,
22863
23065
  telemetry: this.config.telemetry,
22864
23066
  retryPolicy: this.config.retryPolicy
@@ -22910,7 +23112,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22910
23112
  if (!this.currentSessionCoversAccountRegistry()) {
22911
23113
  if (!_TinyCloudNode.accountRegistrySkipLogged) {
22912
23114
  _TinyCloudNode.accountRegistrySkipLogged = true;
22913
- import_sdk_services2.tinyCloudDebugLogger.log("registry.skip");
23115
+ import_sdk_services3.tinyCloudDebugLogger.log("registry.skip");
22914
23116
  }
22915
23117
  return;
22916
23118
  }
@@ -22922,10 +23124,11 @@ var _TinyCloudNode = class _TinyCloudNode {
22922
23124
  const promise = this.enqueueAccountRegistryOperation(session, async () => {
22923
23125
  const sharedContext = this._serviceContext;
22924
23126
  if (!sharedContext) return;
22925
- const context = this._serviceGraph.track(new import_sdk_core9.ServiceContext({
23127
+ const context = this._serviceGraph.track(createServiceContext({
23128
+ requestSink: this._serviceGraph.requestSink,
22926
23129
  invoke: sharedContext.invoke,
22927
23130
  invokeAny: sharedContext.invokeAny,
22928
- fetch: sharedContext.fetch,
23131
+ fetch: unrecordedFetchOf(sharedContext),
22929
23132
  hosts: sharedContext.hosts,
22930
23133
  telemetry: this.config.telemetry,
22931
23134
  retryPolicy: this.config.retryPolicy
@@ -23659,6 +23862,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23659
23862
  if (resolvedHost) this.config.host = resolvedHost;
23660
23863
  if (spaceOwnerKey(serviceSession.spaceId) !== this.currentStorageOwner()) {
23661
23864
  this.storageFull.clear();
23865
+ this._requestLog?.clear();
23662
23866
  }
23663
23867
  this._address = stagedAddress;
23664
23868
  this._chainId = stagedChainId;
@@ -23778,7 +23982,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23778
23982
  const coreContext = graph.track(core.serviceContext);
23779
23983
  coreContext.setSession(input.serviceSession);
23780
23984
  }
23781
- const serviceContext = graph.track(new import_sdk_core9.ServiceContext({
23985
+ const serviceContext = graph.track(createServiceContext({
23986
+ requestSink: graph.requestSink,
23782
23987
  invoke: graph.invoke,
23783
23988
  invokeAny: graph.invokeAny,
23784
23989
  fetch: graph.fetch,
@@ -23866,7 +24071,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23866
24071
  },
23867
24072
  createKVService: (config) => {
23868
24073
  const service = new import_sdk_core9.KVService({ prefix: config.pathPrefix?.replace(/\/$/, "") });
23869
- const context = graph.track(new import_sdk_core9.ServiceContext({
24074
+ const context = graph.track(createServiceContext({
24075
+ requestSink: graph.requestSink,
23870
24076
  invoke: config.invoke,
23871
24077
  fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
23872
24078
  hosts: config.hosts,
@@ -23906,7 +24112,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23906
24112
  createKVService: (spaceId) => {
23907
24113
  graph.assertActive();
23908
24114
  const scopedKv = new import_sdk_core9.KVService({});
23909
- const context = graph.track(new import_sdk_core9.ServiceContext({
24115
+ const context = graph.track(createServiceContext({
24116
+ requestSink: graph.requestSink,
23910
24117
  invoke: graph.invoke,
23911
24118
  invokeAny: graph.invokeAny,
23912
24119
  fetch: graph.fetch,
@@ -23922,7 +24129,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23922
24129
  createVaultService: (spaceId) => {
23923
24130
  graph.assertActive();
23924
24131
  const scopedKv = new import_sdk_core9.KVService({});
23925
- const context = graph.track(new import_sdk_core9.ServiceContext({
24132
+ const context = graph.track(createServiceContext({
24133
+ requestSink: graph.requestSink,
23926
24134
  invoke: graph.invoke,
23927
24135
  invokeAny: graph.invokeAny,
23928
24136
  fetch: graph.fetch,
@@ -24321,7 +24529,8 @@ var _TinyCloudNode = class _TinyCloudNode {
24321
24529
  this._serviceGraph.fetch
24322
24530
  );
24323
24531
  this._serviceGraph.track(this.tc.serviceContext);
24324
- this._serviceContext = this._serviceGraph.track(new import_sdk_core9.ServiceContext({
24532
+ this._serviceContext = this._serviceGraph.track(createServiceContext({
24533
+ requestSink: this._serviceGraph.requestSink,
24325
24534
  invoke: this._serviceGraph.invoke,
24326
24535
  invokeAny: this._serviceGraph.invokeAny,
24327
24536
  fetch: this._serviceGraph.fetch,
@@ -24369,10 +24578,11 @@ var _TinyCloudNode = class _TinyCloudNode {
24369
24578
  createSpaceScopedKVService(spaceId) {
24370
24579
  const kvService = new import_sdk_core9.KVService({});
24371
24580
  if (this._serviceContext) {
24372
- const spaceScopedContext = this._serviceGraph.track(new import_sdk_core9.ServiceContext({
24581
+ const spaceScopedContext = this._serviceGraph.track(createServiceContext({
24582
+ requestSink: this._serviceGraph.requestSink,
24373
24583
  invoke: this._serviceContext.invoke,
24374
24584
  invokeAny: this._serviceContext.invokeAny,
24375
- fetch: this._serviceContext.fetch,
24585
+ fetch: unrecordedFetchOf(this._serviceContext),
24376
24586
  hosts: this._serviceContext.hosts,
24377
24587
  telemetry: this.config.telemetry,
24378
24588
  retryPolicy: this.config.retryPolicy
@@ -24560,7 +24770,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24560
24770
  );
24561
24771
  graph.assertActive();
24562
24772
  if (!response.ok) {
24563
- const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services2.parsePermissionHint)({
24773
+ const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services3.parsePermissionHint)({
24564
24774
  service: "tinycloud.encryption",
24565
24775
  path: networkId,
24566
24776
  actions: ["tinycloud.encryption/decrypt"]
@@ -24838,7 +25048,8 @@ var _TinyCloudNode = class _TinyCloudNode {
24838
25048
  createKVService: (config) => {
24839
25049
  graph.assertActive();
24840
25050
  const service = new import_sdk_core9.KVService({ prefix: config.pathPrefix?.replace(/\/$/, "") });
24841
- const context = graph.track(new import_sdk_core9.ServiceContext({
25051
+ const context = graph.track(createServiceContext({
25052
+ requestSink: graph.requestSink,
24842
25053
  invoke: config.invoke,
24843
25054
  fetch: config.fetch ?? graph.fetch,
24844
25055
  hosts: config.hosts,
@@ -25159,10 +25370,11 @@ var _TinyCloudNode = class _TinyCloudNode {
25159
25370
  throw new Error("Not signed in. Call signIn() first.");
25160
25371
  }
25161
25372
  const sql = new import_sdk_core9.SQLService({});
25162
- const spaceScopedContext = this._serviceGraph.track(new import_sdk_core9.ServiceContext({
25373
+ const spaceScopedContext = this._serviceGraph.track(createServiceContext({
25374
+ requestSink: this._serviceGraph.requestSink,
25163
25375
  invoke: this._serviceContext.invoke,
25164
25376
  invokeAny: this._serviceContext.invokeAny,
25165
- fetch: this._serviceContext.fetch,
25377
+ fetch: unrecordedFetchOf(this._serviceContext),
25166
25378
  hosts: this._serviceContext.hosts,
25167
25379
  telemetry: this.config.telemetry,
25168
25380
  retryPolicy: this.config.retryPolicy
@@ -25190,10 +25402,11 @@ var _TinyCloudNode = class _TinyCloudNode {
25190
25402
  throw new Error("Not signed in. Call signIn() first.");
25191
25403
  }
25192
25404
  const kv = new import_sdk_core9.KVService({});
25193
- const spaceScopedContext = this._serviceGraph.track(new import_sdk_core9.ServiceContext({
25405
+ const spaceScopedContext = this._serviceGraph.track(createServiceContext({
25406
+ requestSink: this._serviceGraph.requestSink,
25194
25407
  invoke: this._serviceContext.invoke,
25195
25408
  invokeAny: this._serviceContext.invokeAny,
25196
- fetch: this._serviceContext.fetch,
25409
+ fetch: unrecordedFetchOf(this._serviceContext),
25197
25410
  hosts: this._serviceContext.hosts
25198
25411
  }));
25199
25412
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
@@ -25334,6 +25547,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25334
25547
  getManifest: () => this.manifest,
25335
25548
  hasPermissions: (permissions) => this.hasRuntimePermissions(permissions),
25336
25549
  grantPermissions: (additional) => this.grantRuntimePermissions(additional),
25550
+ grantGetPermissions: (additional) => this.grantRuntimePermissions(additional, { expiry: SECRET_GET_GRANT_EXPIRY_MS }),
25551
+ uncoveredEntries: (permissions) => permissions.filter((entry) => !this.hasRuntimePermissions([entry])),
25337
25552
  canEscalate: () => this.signer !== void 0 && this.tc !== void 0,
25338
25553
  getEncryptionNetworkId: () => this.getEncryptionNetworkIdForSpace(resolvedSpace),
25339
25554
  resolveSpace: (space) => space.startsWith("tinycloud:") ? space : this.ownedSpaceId(space),
@@ -25356,7 +25571,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25356
25571
  );
25357
25572
  if (result.status !== "ok") {
25358
25573
  if (result.status === "permission_required") {
25359
- const hint = (0, import_sdk_services2.parsePermissionHint)(result.hint);
25574
+ const hint = (0, import_sdk_services3.parsePermissionHint)(result.hint);
25360
25575
  if (hint === void 0) return { status: "read_failed" };
25361
25576
  return {
25362
25577
  status: "permission_required",
@@ -25681,6 +25896,13 @@ var _TinyCloudNode = class _TinyCloudNode {
25681
25896
  }
25682
25897
  }
25683
25898
  const expanded = this.expandPermissionEntries(permissions);
25899
+ const requestsDecrypt = includesDecrypt(expanded);
25900
+ const requestedExpiryMs = requestsDecrypt && options?.expiry === void 0 ? DECRYPT_GRANT_MAX_EXPIRY_MS : resolveExpiryMs(options?.expiry);
25901
+ if (requestsDecrypt && requestedExpiryMs > DECRYPT_GRANT_MAX_EXPIRY_MS) {
25902
+ throw Object.assign(new Error("decrypt grants last at most one hour"), {
25903
+ code: "DECRYPT_EXPIRY_TOO_LONG"
25904
+ });
25905
+ }
25684
25906
  if (this.sessionCoversPermissionEntries(session, expanded)) {
25685
25907
  return [];
25686
25908
  }
@@ -25711,7 +25933,6 @@ var _TinyCloudNode = class _TinyCloudNode {
25711
25933
  bySpace.set(session.spaceId, []);
25712
25934
  }
25713
25935
  const now = /* @__PURE__ */ new Date();
25714
- const requestedExpiryMs = resolveExpiryMs(options?.expiry);
25715
25936
  let expiresAt = new Date(now.getTime() + requestedExpiryMs);
25716
25937
  if (sessionExpiry !== void 0 && sessionExpiry < expiresAt) {
25717
25938
  expiresAt = sessionExpiry;
@@ -25979,7 +26200,8 @@ var _TinyCloudNode = class _TinyCloudNode {
25979
26200
  }
25980
26201
  if (this._serviceContext) {
25981
26202
  const publicKV = new import_sdk_core9.KVService({ prefix: "" });
25982
- const publicContext = this._serviceGraph.track(new import_sdk_core9.ServiceContext({
26203
+ const publicContext = this._serviceGraph.track(createServiceContext({
26204
+ requestSink: this._serviceGraph.requestSink,
25983
26205
  invoke: this._serviceGraph.invoke,
25984
26206
  fetch: this._serviceGraph.fetch,
25985
26207
  hosts: this._serviceContext.hosts,
@@ -27699,7 +27921,7 @@ var import_node_fs = require("fs");
27699
27921
  var import_promises = require("fs/promises");
27700
27922
  var import_node_path = require("path");
27701
27923
  var import_sdk_core13 = require("@tinycloud/sdk-core");
27702
- var import_sdk_services3 = require("@tinycloud/sdk-services");
27924
+ var import_sdk_services4 = require("@tinycloud/sdk-services");
27703
27925
  var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
27704
27926
  init_authority();
27705
27927
  var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
@@ -28261,7 +28483,7 @@ function sha256Base32(material) {
28261
28483
  return out.slice(0, 26);
28262
28484
  }
28263
28485
  function partitionDirOf(root, identity) {
28264
- return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services3.replicationIdentityKey)(identity)));
28486
+ return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services4.replicationIdentityKey)(identity)));
28265
28487
  }
28266
28488
  function replicaHashOf(prefix, deviceDid) {
28267
28489
  return sha256Base32(`${prefix}
@@ -28335,8 +28557,8 @@ function deviceRecordOf(value) {
28335
28557
  }
28336
28558
  function sameIdentity(a, b) {
28337
28559
  try {
28338
- const left = (0, import_sdk_services3.canonicalReplicationIdentity)(a);
28339
- const right = (0, import_sdk_services3.canonicalReplicationIdentity)(b);
28560
+ const left = (0, import_sdk_services4.canonicalReplicationIdentity)(a);
28561
+ const right = (0, import_sdk_services4.canonicalReplicationIdentity)(b);
28340
28562
  return left.host === right.host && left.space === right.space && left.principal === right.principal;
28341
28563
  } catch {
28342
28564
  return false;
@@ -28352,7 +28574,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
28352
28574
  const file = (0, import_node_path.join)(dir, "identity.json");
28353
28575
  const existing = await readJsonFile(file);
28354
28576
  if (existing === void 0) {
28355
- await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services3.canonicalReplicationIdentity)(identity) }, dirSync);
28577
+ await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services4.canonicalReplicationIdentity)(identity) }, dirSync);
28356
28578
  return;
28357
28579
  }
28358
28580
  const record = identityRecordOf(existing);
@@ -28451,7 +28673,7 @@ withGate_fn = async function(section) {
28451
28673
  if (snapshot === void 0) throw gated("gate_unavailable");
28452
28674
  const [principalHash, identityKeyHash] = await Promise.all([
28453
28675
  gate.principalHashOf(snapshot.salt, this.identity.principal),
28454
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(this.identity))
28676
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(this.identity))
28455
28677
  ]);
28456
28678
  const access = {
28457
28679
  stamp: __privateGet(this, _stamp),
@@ -28951,7 +29173,7 @@ function createSqliteReplicaStorage(options) {
28951
29173
  if (snapshot === void 0) throw gated("gate_unavailable");
28952
29174
  const [principalHash, identityKeyHash] = await Promise.all([
28953
29175
  gate.principalHashOf(snapshot.salt, principal),
28954
- identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(identity))
29176
+ identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(identity))
28955
29177
  ]);
28956
29178
  return profileSection(async () => {
28957
29179
  assertLogoutClear(options.logoutPendingFile);
@@ -28974,7 +29196,7 @@ function createSqliteReplicaStorage(options) {
28974
29196
  if (snapshot === void 0) throw gated("gate_unavailable");
28975
29197
  const [principalHash, identityKeyHash] = await Promise.all([
28976
29198
  gate.principalHashOf(snapshot.salt, spec.identity.principal),
28977
- gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity))
29199
+ gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(spec.identity))
28978
29200
  ]);
28979
29201
  const access = {
28980
29202
  stamp: spec.stamp,
@@ -29058,7 +29280,7 @@ function createSqliteReplicaStorage(options) {
29058
29280
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
29059
29281
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
29060
29282
  if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
29061
- const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
29283
+ const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services4.replicationIdentityKey)(identity));
29062
29284
  const recordedAt = Date.now();
29063
29285
  if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
29064
29286
  intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
@@ -29736,7 +29958,7 @@ function createSqliteReplicaStorage(options) {
29736
29958
  for (const dir of await identityDirs()) {
29737
29959
  const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
29738
29960
  if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
29739
- const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
29961
+ const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services4.replicationIdentityKey)(identity));
29740
29962
  if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
29741
29963
  idDir = dir;
29742
29964
  break;
@@ -29882,7 +30104,7 @@ function createSqliteReplicaStorage(options) {
29882
30104
  const gate = await fileGate();
29883
30105
  const snapshot = await gate.readGateSnapshot(profileDir);
29884
30106
  if (snapshot === void 0) throw gated("gate_unavailable");
29885
- const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity));
30107
+ const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services4.replicationIdentityKey)(spec.identity));
29886
30108
  const resolvePolicy = async (at, recordFirst) => {
29887
30109
  const policy = spec.grantPolicy;
29888
30110
  if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
@@ -29904,7 +30126,7 @@ function createSqliteReplicaStorage(options) {
29904
30126
  let partitionId;
29905
30127
  const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
29906
30128
  return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
29907
- if ((0, import_sdk_services3.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
30129
+ if ((0, import_sdk_services4.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
29908
30130
  throw new replica.ReplicaError(
29909
30131
  replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
29910
30132
  `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
@@ -30150,7 +30372,7 @@ function createSqliteReplicaStorage(options) {
30150
30372
  const gate = await fileGate();
30151
30373
  const currentGate = await gate.readGateSnapshot(profileDir);
30152
30374
  if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
30153
- const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services3.replicationIdentityKey)(target.identity));
30375
+ const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services4.replicationIdentityKey)(target.identity));
30154
30376
  await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
30155
30377
  }
30156
30378
  if (state.revoked === null) {