@oxy.so/core 7.18.0 → 7.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/cjs/HttpService.js +2 -0
  2. package/dist/cjs/OxyServices.errors.js +21 -0
  3. package/dist/cjs/OxyServices.js +86 -75
  4. package/dist/cjs/client/callOptions.js +66 -0
  5. package/dist/cjs/index.js +5 -4
  6. package/dist/cjs/server/OxyServer.js +10 -27
  7. package/dist/cjs/server/namespaces.js +85 -26
  8. package/dist/cjs/server/requesterClient.js +1 -3
  9. package/dist/cjs/server/safeFetch.js +43 -12
  10. package/dist/esm/HttpService.js +2 -1
  11. package/dist/esm/OxyServices.errors.js +20 -0
  12. package/dist/esm/OxyServices.js +86 -75
  13. package/dist/esm/client/callOptions.js +61 -0
  14. package/dist/esm/index.js +1 -1
  15. package/dist/esm/server/OxyServer.js +10 -27
  16. package/dist/esm/server/namespaces.js +86 -27
  17. package/dist/esm/server/requesterClient.js +1 -3
  18. package/dist/esm/server/safeFetch.js +43 -12
  19. package/dist/types/HttpService.d.ts +2 -0
  20. package/dist/types/OxyServices.d.ts +38 -25
  21. package/dist/types/OxyServices.errors.d.ts +6 -0
  22. package/dist/types/client/callOptions.d.ts +45 -0
  23. package/dist/types/index.d.ts +4 -3
  24. package/dist/types/models/interfaces.d.ts +23 -0
  25. package/dist/types/server/OxyServer.d.ts +0 -9
  26. package/dist/types/server/namespaces.d.ts +50 -13
  27. package/dist/types/server/requesterClient.d.ts +0 -1
  28. package/dist/types/server/safeFetch.d.ts +16 -0
  29. package/package.json +1 -1
  30. package/src/HttpService.ts +2 -1
  31. package/src/OxyServices.errors.ts +19 -0
  32. package/src/OxyServices.ts +129 -75
  33. package/src/client/callOptions.ts +88 -0
  34. package/src/index.ts +12 -1
  35. package/src/models/interfaces.ts +25 -0
  36. package/src/server/OxyServer.ts +15 -33
  37. package/src/server/namespaces.ts +128 -43
  38. package/src/server/requesterClient.ts +1 -3
  39. package/src/server/safeFetch.ts +69 -11
@@ -208,6 +208,25 @@ export class OxyApiError extends Error {
208
208
  }
209
209
  }
210
210
 
211
+ /**
212
+ * How long Oxy asked the caller to wait before trying again, in seconds — the
213
+ * `Retry-After` of a 429 (`details.retryAfterSeconds` of its body) — or
214
+ * `undefined` when the error is no such refusal or names no wait.
215
+ */
216
+ export function retryAfterSecondsOf(error: unknown): number | undefined {
217
+ if (!error || typeof error !== 'object') return undefined;
218
+ const candidates = [
219
+ (error as { details?: { retryAfterSeconds?: unknown } }).details,
220
+ (error as { response?: { data?: { details?: { retryAfterSeconds?: unknown } } } }).response
221
+ ?.data?.details,
222
+ ];
223
+ for (const details of candidates) {
224
+ const seconds = details?.retryAfterSeconds;
225
+ if (typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0) return seconds;
226
+ }
227
+ return undefined;
228
+ }
229
+
211
230
  /**
212
231
  * Normalise anything a request rejected with into an `OxyApiError`. An error
213
232
  * that already is one (or a typed SDK error such as `OxyAuthenticationError`)
@@ -32,6 +32,7 @@ import {
32
32
  import type { OxyConfig as OxyConfigBase } from './models/interfaces';
33
33
  import { toOxyApiError } from './OxyServices.errors';
34
34
  import type { HttpMethod, OxyContext } from './client/context';
35
+ import { assertCallOptions, type CallOptions, sendWithCallOptions } from './client/callOptions';
35
36
  import { SessionApi } from './api/session';
36
37
  import { AuthApi } from './api/auth';
37
38
  import { UsersApi } from './api/users';
@@ -79,6 +80,42 @@ export const OXY_CLOUD_URL = 'https://cloud.oxy.so';
79
80
  /** Default Oxy API URL. */
80
81
  export const OXY_API_URL = 'https://api.oxy.so';
81
82
 
83
+ /**
84
+ * The namespaces a call scope ({@link OxyServices.withCallOptions}) carries:
85
+ * every one but `session`, which is the client's auth state rather than a call.
86
+ */
87
+ export type OxyCallScopeNamespace =
88
+ | 'auth'
89
+ | 'users'
90
+ | 'follows'
91
+ | 'privacy'
92
+ | 'notifications'
93
+ | 'assets'
94
+ | 'identity'
95
+ | 'safety'
96
+ | 'contentClaims'
97
+ | 'organizations'
98
+ | 'accounts'
99
+ | 'apps'
100
+ | 'linkedAccounts'
101
+ | 'agency'
102
+ | 'store'
103
+ | 'billing'
104
+ | 'reputation'
105
+ | 'civic'
106
+ | 'nodes'
107
+ | 'devices'
108
+ | 'topics'
109
+ | 'appData'
110
+ | 'console'
111
+ | 'contacts';
112
+
113
+ /**
114
+ * The namespaces of a client — an `OxyServer`'s server namespaces included —
115
+ * whose every request carries one set of {@link CallOptions}.
116
+ */
117
+ export type OxyCallScope<T extends OxyServices = OxyServices> = Pick<T, OxyCallScopeNamespace>;
118
+
82
119
  export class OxyServices {
83
120
  /** The transport. Every namespace call goes through it. */
84
121
  readonly http: HttpService;
@@ -216,153 +253,170 @@ export class OxyServices {
216
253
 
217
254
  // ── Namespaces (created on first access) ─────────────────────────────────
218
255
 
219
- private _session?: SessionApi;
220
- private _auth?: AuthApi;
221
- private _users?: UsersApi;
222
- private _follows?: FollowsApi;
223
- private _privacy?: PrivacyApi;
224
- private _notifications?: NotificationsApi;
225
- private _assets?: AssetsApi;
226
- private _identity?: IdentityApi;
227
- private _accounts?: AccountsApi;
228
- private _organizations?: OrganizationsApi;
229
- private _safety?: SafetyApi;
230
- private _apps?: AppsApi;
231
- private _linkedAccounts?: LinkedAccountsApi;
232
- private _agency?: AgencyApi;
233
- private _store?: StoreApi;
234
- private _billing?: BillingApi;
235
- private _reputation?: ReputationApi;
236
- private _civic?: CivicApi;
237
- private _nodes?: NodesApi;
238
- private _devices?: DevicesApi;
239
- private _topics?: TopicsApi;
240
- private _appData?: AppDataApi;
241
- private _contacts?: ContactsApi;
242
- private _console?: ConsoleApi;
243
- private _contentClaims?: ContentClaimsApi;
256
+ /** Each namespace, created on first access and kept for the client's life. */
257
+ private readonly namespaceInstances = new Map<string, unknown>();
258
+
259
+ /** The namespace `name`, created with this client's context on first access. */
260
+ protected namespace<T>(name: string, create: (context: OxyContext) => T): T {
261
+ let instance = this.namespaceInstances.get(name) as T | undefined;
262
+ if (instance === undefined) {
263
+ instance = create(this.context);
264
+ this.namespaceInstances.set(name, instance);
265
+ }
266
+ return instance;
267
+ }
268
+
269
+ /**
270
+ * This client's namespaces with per-call transport options: every request
271
+ * made through them carries `options`, on top of what each method sets.
272
+ *
273
+ * ```ts
274
+ * // One slow write, one tight read — one client.
275
+ * await oxy.withCallOptions({ timeout: 60_000 }).assets.setVisibility(id, 'public');
276
+ * const page = await oxy.withCallOptions({ deadline: 2_500 }).users.byUsername(name);
277
+ * // A server's own namespaces too:
278
+ * await server.withCallOptions({ retry: false }).assets.references.add(refs);
279
+ * ```
280
+ *
281
+ * - `timeout` / `deadline` replace the method's own and the client's.
282
+ * - `retry: false` sends each request once; `retry: true` never re-enables
283
+ * a request the method sends once on purpose.
284
+ * - `signal` aborts the call; a method's own signal still applies too.
285
+ *
286
+ * The scope is a view, not a client: it shares this client's session,
287
+ * cache, credentials and service lane, holds no resource and needs no
288
+ * disposal. A namespace one call makes into another (`privacy` reading
289
+ * `users.me()`) keeps the options. `session` is not part of it.
290
+ */
291
+ withCallOptions(options: CallOptions): OxyCallScope<this> {
292
+ assertCallOptions(options);
293
+ const scope: CallOptions = { ...options };
294
+ const base = this.context;
295
+ const lane = base.service;
296
+ const view = Object.create(this) as this;
297
+ const context: OxyContext = {
298
+ oxy: view,
299
+ http: base.http,
300
+ request: (method, url, data, requestOptions) =>
301
+ sendWithCallOptions(requestOptions, scope, (merged) =>
302
+ base.request(method, url, data, merged),
303
+ ),
304
+ service: lane && {
305
+ get available() {
306
+ return lane.available;
307
+ },
308
+ request: (method, url, data, requestOptions) =>
309
+ sendWithCallOptions(requestOptions, scope, (merged) =>
310
+ lane.request(method, url, data, merged),
311
+ ),
312
+ },
313
+ };
314
+ // The view's own namespaces are built on the scoped context; everything
315
+ // else (http, cache, config, credentials) is this client's, by prototype.
316
+ Object.defineProperties(view, {
317
+ context: { value: context },
318
+ namespaceInstances: { value: new Map<string, unknown>() },
319
+ session: { get: () => this.session },
320
+ });
321
+ return view;
322
+ }
244
323
 
245
324
  /** This client's auth state (token, user id) and its server session. */
246
325
  get session(): SessionApi {
247
- if (!this._session) this._session = new SessionApi(this.context);
248
- return this._session;
326
+ return this.namespace('session', (context) => new SessionApi(context));
249
327
  }
250
328
  /** Signing in, signing up, and the account's credentials. */
251
329
  get auth(): AuthApi {
252
- if (!this._auth) this._auth = new AuthApi(this.context);
253
- return this._auth;
330
+ return this.namespace('auth', (context) => new AuthApi(context));
254
331
  }
255
332
  /** People and their profiles. */
256
333
  get users(): UsersApi {
257
- if (!this._users) this._users = new UsersApi(this.context);
258
- return this._users;
334
+ return this.namespace('users', (context) => new UsersApi(context));
259
335
  }
260
336
  /** The follow graph: people and every other followable target. */
261
337
  get follows(): FollowsApi {
262
- if (!this._follows) this._follows = new FollowsApi(this.context);
263
- return this._follows;
338
+ return this.namespace('follows', (context) => new FollowsApi(context));
264
339
  }
265
340
  /** Privacy settings, blocks and restrictions. */
266
341
  get privacy(): PrivacyApi {
267
- if (!this._privacy) this._privacy = new PrivacyApi(this.context);
268
- return this._privacy;
342
+ return this.namespace('privacy', (context) => new PrivacyApi(context));
269
343
  }
270
344
  /** The in-app inbox and push tokens. */
271
345
  get notifications(): NotificationsApi {
272
- if (!this._notifications) this._notifications = new NotificationsApi(this.context);
273
- return this._notifications;
346
+ return this.namespace('notifications', (context) => new NotificationsApi(context));
274
347
  }
275
348
  /** Files: upload, link, URLs, content. */
276
349
  get assets(): AssetsApi {
277
- if (!this._assets) this._assets = new AssetsApi(this.context);
278
- return this._assets;
350
+ return this.namespace('assets', (context) => new AssetsApi(context));
279
351
  }
280
352
  /** DID, keys, identity links, domains, backup and the signed export. */
281
353
  get identity(): IdentityApi {
282
- if (!this._identity) this._identity = new IdentityApi(this.context);
283
- return this._identity;
354
+ return this.namespace('identity', (context) => new IdentityApi(context));
284
355
  }
285
356
  /** The account graph: accounts and their members. */
286
357
  get safety(): SafetyApi {
287
- if (!this._safety) this._safety = new SafetyApi(this.context);
288
- return this._safety;
358
+ return this.namespace('safety', (context) => new SafetyApi(context));
289
359
  }
290
360
  /** Content claims: an owner's counter-notice to a takedown (ADR 0043 D10). */
291
361
  get contentClaims(): ContentClaimsApi {
292
- if (!this._contentClaims) this._contentClaims = new ContentClaimsApi(this.context);
293
- return this._contentClaims;
362
+ return this.namespace('contentClaims', (context) => new ContentClaimsApi(context));
294
363
  }
295
364
 
296
365
  get organizations(): OrganizationsApi {
297
- if (!this._organizations) this._organizations = new OrganizationsApi(this.context);
298
- return this._organizations;
366
+ return this.namespace('organizations', (context) => new OrganizationsApi(context));
299
367
  }
300
368
 
301
369
  get accounts(): AccountsApi {
302
- if (!this._accounts) this._accounts = new AccountsApi(this.context);
303
- return this._accounts;
370
+ return this.namespace('accounts', (context) => new AccountsApi(context));
304
371
  }
305
372
  /** Applications an account owns, and the ones this user granted access. */
306
373
  get apps(): AppsApi {
307
- if (!this._apps) this._apps = new AppsApi(this.context);
308
- return this._apps;
374
+ return this.namespace('apps', (context) => new AppsApi(context));
309
375
  }
310
376
  /** External accounts (Mastodon, Bluesky) linked by OAuth. */
311
377
  get linkedAccounts(): LinkedAccountsApi {
312
- if (!this._linkedAccounts) this._linkedAccounts = new LinkedAccountsApi(this.context);
313
- return this._linkedAccounts;
378
+ return this.namespace('linkedAccounts', (context) => new LinkedAccountsApi(context));
314
379
  }
315
380
  /** Delegated capabilities for agents. */
316
381
  get agency(): AgencyApi {
317
- if (!this._agency) this._agency = new AgencyApi(this.context);
318
- return this._agency;
382
+ return this.namespace('agency', (context) => new AgencyApi(context));
319
383
  }
320
384
  /** The app store: storefront, reviews, and a publisher's listing. */
321
385
  get store(): StoreApi {
322
- if (!this._store) this._store = new StoreApi(this.context);
323
- return this._store;
386
+ return this.namespace('store', (context) => new StoreApi(context));
324
387
  }
325
388
  /** Subscription, wallet and payments. */
326
389
  get billing(): BillingApi {
327
- if (!this._billing) this._billing = new BillingApi(this.context);
328
- return this._billing;
390
+ return this.namespace('billing', (context) => new BillingApi(context));
329
391
  }
330
392
  /** Oxy Trust reputation (read-only for people; apps award it). */
331
393
  get reputation(): ReputationApi {
332
- if (!this._reputation) this._reputation = new ReputationApi(this.context);
333
- return this._reputation;
394
+ return this.namespace('reputation', (context) => new ReputationApi(context));
334
395
  }
335
396
  /** Oxy ID: cards, attestations, vouching, personhood, credentials. */
336
397
  get civic(): CivicApi {
337
- if (!this._civic) this._civic = new CivicApi(this.context);
338
- return this._civic;
398
+ return this.namespace('civic', (context) => new CivicApi(context));
339
399
  }
340
400
  /** The user's personal data node. */
341
401
  get nodes(): NodesApi {
342
- if (!this._nodes) this._nodes = new NodesApi(this.context);
343
- return this._nodes;
402
+ return this.namespace('nodes', (context) => new NodesApi(context));
344
403
  }
345
404
  /** This user's devices, their sessions, and device sign-in. */
346
405
  get devices(): DevicesApi {
347
- if (!this._devices) this._devices = new DevicesApi(this.context);
348
- return this._devices;
406
+ return this.namespace('devices', (context) => new DevicesApi(context));
349
407
  }
350
408
  get topics(): TopicsApi {
351
- if (!this._topics) this._topics = new TopicsApi(this.context);
352
- return this._topics;
409
+ return this.namespace('topics', (context) => new TopicsApi(context));
353
410
  }
354
411
  /** Per-app key/value data stored on the user's account. */
355
412
  get appData(): AppDataApi {
356
- if (!this._appData) this._appData = new AppDataApi(this.context);
357
- return this._appData;
413
+ return this.namespace('appData', (context) => new AppDataApi(context));
358
414
  }
359
415
  /** Oxy Console: first-run onboarding (terms and use cases) for the signed-in person. */
360
416
  get console(): ConsoleApi {
361
- if (!this._console) this._console = new ConsoleApi(this.context);
362
- return this._console;
417
+ return this.namespace('console', (context) => new ConsoleApi(context));
363
418
  }
364
419
  get contacts(): ContactsApi {
365
- if (!this._contacts) this._contacts = new ContactsApi(this.context);
366
- return this._contacts;
420
+ return this.namespace('contacts', (context) => new ContactsApi(context));
367
421
  }
368
422
  }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Per-call transport options, for any namespace call.
3
+ *
4
+ * `oxy.withCallOptions({ timeout: 60_000 }).assets.setVisibility(id, 'public')`
5
+ * sends that one call with its own budget, without a second client and without
6
+ * every method growing an options parameter of its own. The scope is a view of
7
+ * the same client: same session, cache, service lane and namespaces' state.
8
+ */
9
+ import { linkAbort, type RequestOptions } from '../HttpService';
10
+
11
+ /**
12
+ * Transport options for the calls made through `withCallOptions`, overriding
13
+ * the client's `requestTimeout` / `enableRetry` for those calls only.
14
+ */
15
+ export interface CallOptions {
16
+ /** Abort the call. Combined with any signal the method passes itself. */
17
+ signal?: AbortSignal;
18
+ /** Per-attempt timeout in ms. Replaces the method's own default. */
19
+ timeout?: number;
20
+ /**
21
+ * Wall-clock budget in ms for the whole call, retries and backoff included
22
+ * (`RequestOptions.deadline`) — "never more than 2.5 s" in one number.
23
+ */
24
+ deadline?: number;
25
+ /**
26
+ * `false` sends each request once; a transient failure is the caller's to
27
+ * retry (an outbox). `true` allows the transport's retries where the client
28
+ * turned them off — but never on a request the method sends once on purpose
29
+ * (a non-idempotent write whose lost answer would duplicate it).
30
+ */
31
+ retry?: boolean;
32
+ }
33
+
34
+ /** Throws for a budget the transport cannot honour (0, negative, NaN, Infinity). */
35
+ export function assertCallOptions(options: CallOptions): void {
36
+ for (const key of ['timeout', 'deadline'] as const) {
37
+ const value = options[key];
38
+ if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
39
+ throw new RangeError(`${key} must be a positive number of milliseconds`);
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * The request options a method chose, with the scope's applied on top. The
46
+ * signal is NOT merged here: two signals need a linked controller that lives
47
+ * exactly as long as the request ({@link sendWithCallOptions}).
48
+ */
49
+ export function applyCallOptions<T extends RequestOptions>(
50
+ method: T | undefined,
51
+ scope: CallOptions,
52
+ ): T {
53
+ const merged = { ...method } as T;
54
+ if (scope.timeout !== undefined) merged.timeout = scope.timeout;
55
+ if (scope.deadline !== undefined) merged.deadline = scope.deadline;
56
+ if (scope.retry === false) merged.retry = false;
57
+ else if (scope.retry === true && merged.retry === undefined) merged.retry = true;
58
+ return merged;
59
+ }
60
+
61
+ /**
62
+ * Send one request with the scope applied: `send` receives the merged
63
+ * options, with a signal that aborts when either the method's or the scope's
64
+ * does. The link between them is released when the request settles.
65
+ */
66
+ export async function sendWithCallOptions<T, O extends RequestOptions>(
67
+ method: O | undefined,
68
+ scope: CallOptions,
69
+ send: (options: O) => Promise<T>,
70
+ ): Promise<T> {
71
+ const merged = applyCallOptions(method, scope);
72
+ const own = method?.signal;
73
+ if (!scope.signal || !own || own === scope.signal) {
74
+ const signal = scope.signal ?? own;
75
+ if (signal) merged.signal = signal;
76
+ return send(merged);
77
+ }
78
+ const controller = new AbortController();
79
+ const unlinkOwn = linkAbort(own, controller);
80
+ const unlinkScope = linkAbort(scope.signal, controller);
81
+ merged.signal = controller.signal;
82
+ try {
83
+ return await send(merged);
84
+ } finally {
85
+ unlinkOwn();
86
+ unlinkScope();
87
+ }
88
+ }
package/src/index.ts CHANGED
@@ -31,13 +31,20 @@ import './crypto/polyfill';
31
31
  // API client
32
32
  // ---------------------------------------------------------------------------
33
33
  export { OxyServices, OXY_API_URL, OXY_CLOUD_URL } from './OxyServices';
34
- export type { OxyConfig, LinkedHttpClient } from './OxyServices';
34
+ export type {
35
+ OxyConfig,
36
+ LinkedHttpClient,
37
+ OxyCallScope,
38
+ OxyCallScopeNamespace,
39
+ } from './OxyServices';
40
+ export type { CallOptions } from './client/callOptions';
35
41
  export {
36
42
  OxyApiError,
37
43
  OxyAuthenticationError,
38
44
  OxyAuthenticationTimeoutError,
39
45
  AssetUrlResolutionError,
40
46
  ServiceAssetMetadataError,
47
+ retryAfterSecondsOf,
41
48
  } from './OxyServices.errors';
42
49
  export type { HttpMethod } from './client/context';
43
50
  export type {
@@ -407,6 +414,10 @@ export type {
407
414
  ServiceReferenceInventoryItem,
408
415
  ServiceReferenceReconcileResult,
409
416
  ServiceUserMediaFile,
417
+ ServiceMediaCacheUpload,
418
+ ServiceFederatedMediaUpload,
419
+ ServiceFederatedMediaDeleteResult,
420
+ ServiceFederatedMediaDeletion,
410
421
  FileImportOutcome,
411
422
  FileImportSource,
412
423
  FileImportOptions,
@@ -1041,6 +1041,31 @@ export interface ServiceUserMediaFile {
1041
1041
  visibility: FileVisibility;
1042
1042
  }
1043
1043
 
1044
+ /** `POST /assets/service/cache`: the file now in the federation media cache. */
1045
+ export interface ServiceMediaCacheUpload {
1046
+ file: { id: string };
1047
+ }
1048
+
1049
+ /** `POST /assets/service/federation`: durable federated media, owned by the federated user. */
1050
+ export interface ServiceFederatedMediaUpload {
1051
+ file: ServiceUserMediaFile;
1052
+ /** The owner already held these bytes from this application: the existing file, maybe shared. */
1053
+ deduplicated: boolean;
1054
+ }
1055
+
1056
+ /**
1057
+ * Oxy's answer for one id of `assets.federatedMedia.delete`: `deleted` (this
1058
+ * call removed it), `not_found` (nothing to delete), `in_use` (kept: another
1059
+ * account holds the same bytes) or `forbidden` (not federated media this
1060
+ * application uploaded; left untouched).
1061
+ */
1062
+ export type ServiceFederatedMediaDeleteResult = 'deleted' | 'not_found' | 'in_use' | 'forbidden';
1063
+
1064
+ export interface ServiceFederatedMediaDeletion {
1065
+ id: string;
1066
+ result: ServiceFederatedMediaDeleteResult;
1067
+ }
1068
+
1044
1069
  /**
1045
1070
  * How Oxy renders an AUDIO file it stores (ADR 0043 stage 4c). Every audio
1046
1071
  * file gets an audio-only HLS ladder (AAC 64k/128k/256k, a rung above the
@@ -552,65 +552,47 @@ export class OxyServer extends OxyServices {
552
552
 
553
553
  // ── Server-side namespaces ───────────────────────────────────────────────
554
554
 
555
- private _serverAccounts?: ServerAccountsApi;
556
- private _serverAssets?: ServerAssetsApi;
557
- private _serverNotifications?: ServerNotificationsApi;
558
- private _serverLinkedAccounts?: ServerLinkedAccountsApi;
559
- private _serverAgency?: ServerAgencyApi;
560
- private _serverApps?: ServerAppsApi;
561
- private _serverReputation?: ServerReputationApi;
562
- private _serverSafety?: ServerSafetyApi;
563
- private _serverSession?: ServerSessionApi;
564
-
565
555
  /** The account graph, plus the service-only `accessFor` membership verdict. */
566
556
  override get accounts(): ServerAccountsApi {
567
- if (!this._serverAccounts) this._serverAccounts = new ServerAccountsApi(this.context);
568
- return this._serverAccounts;
557
+ return this.namespace('accounts', (context) => new ServerAccountsApi(context));
569
558
  }
570
559
  /** Files, plus the service-only metadata and linked-URL lookups. */
571
560
  override get assets(): ServerAssetsApi {
572
- if (!this._serverAssets) this._serverAssets = new ServerAssetsApi(this.context);
573
- return this._serverAssets;
561
+ return this.namespace('assets', (context) => new ServerAssetsApi(context));
574
562
  }
575
563
  /** The inbox and push tokens, plus `create`. */
576
564
  override get notifications(): ServerNotificationsApi {
577
- if (!this._serverNotifications)
578
- this._serverNotifications = new ServerNotificationsApi(this.context);
579
- return this._serverNotifications;
565
+ return this.namespace('notifications', (context) => new ServerNotificationsApi(context));
580
566
  }
581
567
  /** Linked external accounts, plus `forUser`. */
582
568
  override get linkedAccounts(): ServerLinkedAccountsApi {
583
- if (!this._serverLinkedAccounts)
584
- this._serverLinkedAccounts = new ServerLinkedAccountsApi(this.context);
585
- return this._serverLinkedAccounts;
569
+ return this.namespace('linkedAccounts', (context) => new ServerLinkedAccountsApi(context));
586
570
  }
587
571
  /** Delegated capabilities, with the service-only authority calls. */
588
572
  override get agency(): ServerAgencyApi {
589
- if (!this._serverAgency) this._serverAgency = new ServerAgencyApi(this.context);
590
- return this._serverAgency;
573
+ return this.namespace('agency', (context) => new ServerAgencyApi(context));
591
574
  }
592
575
  /** Application management, plus the receiver-bound Alia machine-key lookup. */
593
576
  override get apps(): ServerAppsApi {
594
- if (!this._serverApps) this._serverApps = new ServerAppsApi(this.context);
595
- return this._serverApps;
577
+ return this.namespace('apps', (context) => new ServerAppsApi(context));
596
578
  }
597
579
  /** Reputation reads, plus `award`. */
598
580
  override get reputation(): ServerReputationApi {
599
- if (!this._serverReputation) this._serverReputation = new ServerReputationApi(this.context);
600
- return this._serverReputation;
581
+ return this.namespace('reputation', (context) => new ServerReputationApi(context));
601
582
  }
602
583
  /** Policy reads, plus the consumer-service decision lanes (ADR 0041). */
603
584
  override get safety(): ServerSafetyApi {
604
- if (!this._serverSafety) this._serverSafety = new ServerSafetyApi(this.context);
605
- return this._serverSafety;
585
+ return this.namespace('safety', (context) => new ServerSafetyApi(context));
606
586
  }
607
587
  /** Session state, plus validation through a reviewed supervised action. */
608
588
  override get session(): ServerSessionApi {
609
- if (!this._serverSession)
610
- this._serverSession = new ServerSessionApi(this.context, this.sessionValidationTtlMs, () =>
611
- this.revocationFeedEpoch(),
612
- );
613
- return this._serverSession;
589
+ return this.namespace(
590
+ 'session',
591
+ (context) =>
592
+ new ServerSessionApi(context, this.sessionValidationTtlMs, () =>
593
+ this.revocationFeedEpoch(),
594
+ ),
595
+ );
614
596
  }
615
597
 
616
598
  /** Close the revocation feed along with the HTTP client. */