@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
@@ -15,6 +15,7 @@
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.HttpService = void 0;
18
+ exports.linkAbort = linkAbort;
18
19
  const cache_1 = require("./utils/cache");
19
20
  const requestUtils_1 = require("./utils/requestUtils");
20
21
  const asyncUtils_1 = require("./utils/asyncUtils");
@@ -301,6 +302,7 @@ function isAbortLike(error) {
301
302
  error !== null &&
302
303
  error.name === 'AbortError');
303
304
  }
305
+ /** @internal Shared with `client/callOptions.ts`; see the note above `isAbortLike`. */
304
306
  function linkAbort(parent, child) {
305
307
  if (parent.aborted) {
306
308
  child.abort(parent.reason);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OxyApiError = exports.OxyAuthenticationTimeoutError = exports.ServiceLinkedDownloadUrlError = exports.ServiceAssetMetadataError = exports.AssetUrlResolutionError = exports.OxyAuthenticationError = void 0;
4
+ exports.retryAfterSecondsOf = retryAfterSecondsOf;
4
5
  exports.toOxyApiError = toOxyApiError;
5
6
  const errorUtils_1 = require("./utils/errorUtils");
6
7
  /**
@@ -146,6 +147,26 @@ class OxyApiError extends Error {
146
147
  }
147
148
  }
148
149
  exports.OxyApiError = OxyApiError;
150
+ /**
151
+ * How long Oxy asked the caller to wait before trying again, in seconds — the
152
+ * `Retry-After` of a 429 (`details.retryAfterSeconds` of its body) — or
153
+ * `undefined` when the error is no such refusal or names no wait.
154
+ */
155
+ function retryAfterSecondsOf(error) {
156
+ if (!error || typeof error !== 'object')
157
+ return undefined;
158
+ const candidates = [
159
+ error.details,
160
+ error.response
161
+ ?.data?.details,
162
+ ];
163
+ for (const details of candidates) {
164
+ const seconds = details?.retryAfterSeconds;
165
+ if (typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0)
166
+ return seconds;
167
+ }
168
+ return undefined;
169
+ }
149
170
  /**
150
171
  * Normalise anything a request rejected with into an `OxyApiError`. An error
151
172
  * that already is one (or a typed SDK error such as `OxyAuthenticationError`)
@@ -28,6 +28,7 @@ const contentClaims_1 = require("./api/contentClaims");
28
28
  */
29
29
  const HttpService_1 = require("./HttpService");
30
30
  const OxyServices_errors_1 = require("./OxyServices.errors");
31
+ const callOptions_1 = require("./client/callOptions");
31
32
  const session_1 = require("./api/session");
32
33
  const auth_1 = require("./api/auth");
33
34
  const users_1 = require("./api/users");
@@ -67,6 +68,9 @@ class OxyServices {
67
68
  deletePrefix: (prefix) => this.http.clearCacheByPrefix(prefix),
68
69
  stats: () => this.http.getCacheStats(),
69
70
  };
71
+ // ── Namespaces (created on first access) ─────────────────────────────────
72
+ /** Each namespace, created on first access and kept for the client's life. */
73
+ this.namespaceInstances = new Map();
70
74
  if (!config || typeof config !== 'object') {
71
75
  throw new Error('OxyConfig is required');
72
76
  }
@@ -169,151 +173,158 @@ class OxyServices {
169
173
  dispose() {
170
174
  this.http.dispose();
171
175
  }
176
+ /** The namespace `name`, created with this client's context on first access. */
177
+ namespace(name, create) {
178
+ let instance = this.namespaceInstances.get(name);
179
+ if (instance === undefined) {
180
+ instance = create(this.context);
181
+ this.namespaceInstances.set(name, instance);
182
+ }
183
+ return instance;
184
+ }
185
+ /**
186
+ * This client's namespaces with per-call transport options: every request
187
+ * made through them carries `options`, on top of what each method sets.
188
+ *
189
+ * ```ts
190
+ * // One slow write, one tight read — one client.
191
+ * await oxy.withCallOptions({ timeout: 60_000 }).assets.setVisibility(id, 'public');
192
+ * const page = await oxy.withCallOptions({ deadline: 2_500 }).users.byUsername(name);
193
+ * // A server's own namespaces too:
194
+ * await server.withCallOptions({ retry: false }).assets.references.add(refs);
195
+ * ```
196
+ *
197
+ * - `timeout` / `deadline` replace the method's own and the client's.
198
+ * - `retry: false` sends each request once; `retry: true` never re-enables
199
+ * a request the method sends once on purpose.
200
+ * - `signal` aborts the call; a method's own signal still applies too.
201
+ *
202
+ * The scope is a view, not a client: it shares this client's session,
203
+ * cache, credentials and service lane, holds no resource and needs no
204
+ * disposal. A namespace one call makes into another (`privacy` reading
205
+ * `users.me()`) keeps the options. `session` is not part of it.
206
+ */
207
+ withCallOptions(options) {
208
+ (0, callOptions_1.assertCallOptions)(options);
209
+ const scope = { ...options };
210
+ const base = this.context;
211
+ const lane = base.service;
212
+ const view = Object.create(this);
213
+ const context = {
214
+ oxy: view,
215
+ http: base.http,
216
+ request: (method, url, data, requestOptions) => (0, callOptions_1.sendWithCallOptions)(requestOptions, scope, (merged) => base.request(method, url, data, merged)),
217
+ service: lane && {
218
+ get available() {
219
+ return lane.available;
220
+ },
221
+ request: (method, url, data, requestOptions) => (0, callOptions_1.sendWithCallOptions)(requestOptions, scope, (merged) => lane.request(method, url, data, merged)),
222
+ },
223
+ };
224
+ // The view's own namespaces are built on the scoped context; everything
225
+ // else (http, cache, config, credentials) is this client's, by prototype.
226
+ Object.defineProperties(view, {
227
+ context: { value: context },
228
+ namespaceInstances: { value: new Map() },
229
+ session: { get: () => this.session },
230
+ });
231
+ return view;
232
+ }
172
233
  /** This client's auth state (token, user id) and its server session. */
173
234
  get session() {
174
- if (!this._session)
175
- this._session = new session_1.SessionApi(this.context);
176
- return this._session;
235
+ return this.namespace('session', (context) => new session_1.SessionApi(context));
177
236
  }
178
237
  /** Signing in, signing up, and the account's credentials. */
179
238
  get auth() {
180
- if (!this._auth)
181
- this._auth = new auth_1.AuthApi(this.context);
182
- return this._auth;
239
+ return this.namespace('auth', (context) => new auth_1.AuthApi(context));
183
240
  }
184
241
  /** People and their profiles. */
185
242
  get users() {
186
- if (!this._users)
187
- this._users = new users_1.UsersApi(this.context);
188
- return this._users;
243
+ return this.namespace('users', (context) => new users_1.UsersApi(context));
189
244
  }
190
245
  /** The follow graph: people and every other followable target. */
191
246
  get follows() {
192
- if (!this._follows)
193
- this._follows = new follows_1.FollowsApi(this.context);
194
- return this._follows;
247
+ return this.namespace('follows', (context) => new follows_1.FollowsApi(context));
195
248
  }
196
249
  /** Privacy settings, blocks and restrictions. */
197
250
  get privacy() {
198
- if (!this._privacy)
199
- this._privacy = new privacy_1.PrivacyApi(this.context);
200
- return this._privacy;
251
+ return this.namespace('privacy', (context) => new privacy_1.PrivacyApi(context));
201
252
  }
202
253
  /** The in-app inbox and push tokens. */
203
254
  get notifications() {
204
- if (!this._notifications)
205
- this._notifications = new notifications_1.NotificationsApi(this.context);
206
- return this._notifications;
255
+ return this.namespace('notifications', (context) => new notifications_1.NotificationsApi(context));
207
256
  }
208
257
  /** Files: upload, link, URLs, content. */
209
258
  get assets() {
210
- if (!this._assets)
211
- this._assets = new assets_1.AssetsApi(this.context);
212
- return this._assets;
259
+ return this.namespace('assets', (context) => new assets_1.AssetsApi(context));
213
260
  }
214
261
  /** DID, keys, identity links, domains, backup and the signed export. */
215
262
  get identity() {
216
- if (!this._identity)
217
- this._identity = new identity_1.IdentityApi(this.context);
218
- return this._identity;
263
+ return this.namespace('identity', (context) => new identity_1.IdentityApi(context));
219
264
  }
220
265
  /** The account graph: accounts and their members. */
221
266
  get safety() {
222
- if (!this._safety)
223
- this._safety = new safety_1.SafetyApi(this.context);
224
- return this._safety;
267
+ return this.namespace('safety', (context) => new safety_1.SafetyApi(context));
225
268
  }
226
269
  /** Content claims: an owner's counter-notice to a takedown (ADR 0043 D10). */
227
270
  get contentClaims() {
228
- if (!this._contentClaims)
229
- this._contentClaims = new contentClaims_1.ContentClaimsApi(this.context);
230
- return this._contentClaims;
271
+ return this.namespace('contentClaims', (context) => new contentClaims_1.ContentClaimsApi(context));
231
272
  }
232
273
  get organizations() {
233
- if (!this._organizations)
234
- this._organizations = new organizations_1.OrganizationsApi(this.context);
235
- return this._organizations;
274
+ return this.namespace('organizations', (context) => new organizations_1.OrganizationsApi(context));
236
275
  }
237
276
  get accounts() {
238
- if (!this._accounts)
239
- this._accounts = new accounts_1.AccountsApi(this.context);
240
- return this._accounts;
277
+ return this.namespace('accounts', (context) => new accounts_1.AccountsApi(context));
241
278
  }
242
279
  /** Applications an account owns, and the ones this user granted access. */
243
280
  get apps() {
244
- if (!this._apps)
245
- this._apps = new apps_1.AppsApi(this.context);
246
- return this._apps;
281
+ return this.namespace('apps', (context) => new apps_1.AppsApi(context));
247
282
  }
248
283
  /** External accounts (Mastodon, Bluesky) linked by OAuth. */
249
284
  get linkedAccounts() {
250
- if (!this._linkedAccounts)
251
- this._linkedAccounts = new linkedAccounts_1.LinkedAccountsApi(this.context);
252
- return this._linkedAccounts;
285
+ return this.namespace('linkedAccounts', (context) => new linkedAccounts_1.LinkedAccountsApi(context));
253
286
  }
254
287
  /** Delegated capabilities for agents. */
255
288
  get agency() {
256
- if (!this._agency)
257
- this._agency = new agency_1.AgencyApi(this.context);
258
- return this._agency;
289
+ return this.namespace('agency', (context) => new agency_1.AgencyApi(context));
259
290
  }
260
291
  /** The app store: storefront, reviews, and a publisher's listing. */
261
292
  get store() {
262
- if (!this._store)
263
- this._store = new store_1.StoreApi(this.context);
264
- return this._store;
293
+ return this.namespace('store', (context) => new store_1.StoreApi(context));
265
294
  }
266
295
  /** Subscription, wallet and payments. */
267
296
  get billing() {
268
- if (!this._billing)
269
- this._billing = new billing_1.BillingApi(this.context);
270
- return this._billing;
297
+ return this.namespace('billing', (context) => new billing_1.BillingApi(context));
271
298
  }
272
299
  /** Oxy Trust reputation (read-only for people; apps award it). */
273
300
  get reputation() {
274
- if (!this._reputation)
275
- this._reputation = new reputation_1.ReputationApi(this.context);
276
- return this._reputation;
301
+ return this.namespace('reputation', (context) => new reputation_1.ReputationApi(context));
277
302
  }
278
303
  /** Oxy ID: cards, attestations, vouching, personhood, credentials. */
279
304
  get civic() {
280
- if (!this._civic)
281
- this._civic = new civic_1.CivicApi(this.context);
282
- return this._civic;
305
+ return this.namespace('civic', (context) => new civic_1.CivicApi(context));
283
306
  }
284
307
  /** The user's personal data node. */
285
308
  get nodes() {
286
- if (!this._nodes)
287
- this._nodes = new nodes_1.NodesApi(this.context);
288
- return this._nodes;
309
+ return this.namespace('nodes', (context) => new nodes_1.NodesApi(context));
289
310
  }
290
311
  /** This user's devices, their sessions, and device sign-in. */
291
312
  get devices() {
292
- if (!this._devices)
293
- this._devices = new devices_1.DevicesApi(this.context);
294
- return this._devices;
313
+ return this.namespace('devices', (context) => new devices_1.DevicesApi(context));
295
314
  }
296
315
  get topics() {
297
- if (!this._topics)
298
- this._topics = new topics_1.TopicsApi(this.context);
299
- return this._topics;
316
+ return this.namespace('topics', (context) => new topics_1.TopicsApi(context));
300
317
  }
301
318
  /** Per-app key/value data stored on the user's account. */
302
319
  get appData() {
303
- if (!this._appData)
304
- this._appData = new appData_1.AppDataApi(this.context);
305
- return this._appData;
320
+ return this.namespace('appData', (context) => new appData_1.AppDataApi(context));
306
321
  }
307
322
  /** Oxy Console: first-run onboarding (terms and use cases) for the signed-in person. */
308
323
  get console() {
309
- if (!this._console)
310
- this._console = new console_1.ConsoleApi(this.context);
311
- return this._console;
324
+ return this.namespace('console', (context) => new console_1.ConsoleApi(context));
312
325
  }
313
326
  get contacts() {
314
- if (!this._contacts)
315
- this._contacts = new contacts_1.ContactsApi(this.context);
316
- return this._contacts;
327
+ return this.namespace('contacts', (context) => new contacts_1.ContactsApi(context));
317
328
  }
318
329
  }
319
330
  exports.OxyServices = OxyServices;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertCallOptions = assertCallOptions;
4
+ exports.applyCallOptions = applyCallOptions;
5
+ exports.sendWithCallOptions = sendWithCallOptions;
6
+ /**
7
+ * Per-call transport options, for any namespace call.
8
+ *
9
+ * `oxy.withCallOptions({ timeout: 60_000 }).assets.setVisibility(id, 'public')`
10
+ * sends that one call with its own budget, without a second client and without
11
+ * every method growing an options parameter of its own. The scope is a view of
12
+ * the same client: same session, cache, service lane and namespaces' state.
13
+ */
14
+ const HttpService_1 = require("../HttpService");
15
+ /** Throws for a budget the transport cannot honour (0, negative, NaN, Infinity). */
16
+ function assertCallOptions(options) {
17
+ for (const key of ['timeout', 'deadline']) {
18
+ const value = options[key];
19
+ if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
20
+ throw new RangeError(`${key} must be a positive number of milliseconds`);
21
+ }
22
+ }
23
+ }
24
+ /**
25
+ * The request options a method chose, with the scope's applied on top. The
26
+ * signal is NOT merged here: two signals need a linked controller that lives
27
+ * exactly as long as the request ({@link sendWithCallOptions}).
28
+ */
29
+ function applyCallOptions(method, scope) {
30
+ const merged = { ...method };
31
+ if (scope.timeout !== undefined)
32
+ merged.timeout = scope.timeout;
33
+ if (scope.deadline !== undefined)
34
+ merged.deadline = scope.deadline;
35
+ if (scope.retry === false)
36
+ merged.retry = false;
37
+ else if (scope.retry === true && merged.retry === undefined)
38
+ merged.retry = true;
39
+ return merged;
40
+ }
41
+ /**
42
+ * Send one request with the scope applied: `send` receives the merged
43
+ * options, with a signal that aborts when either the method's or the scope's
44
+ * does. The link between them is released when the request settles.
45
+ */
46
+ async function sendWithCallOptions(method, scope, send) {
47
+ const merged = applyCallOptions(method, scope);
48
+ const own = method?.signal;
49
+ if (!scope.signal || !own || own === scope.signal) {
50
+ const signal = scope.signal ?? own;
51
+ if (signal)
52
+ merged.signal = signal;
53
+ return send(merged);
54
+ }
55
+ const controller = new AbortController();
56
+ const unlinkOwn = (0, HttpService_1.linkAbort)(own, controller);
57
+ const unlinkScope = (0, HttpService_1.linkAbort)(scope.signal, controller);
58
+ merged.signal = controller.signal;
59
+ try {
60
+ return await send(merged);
61
+ }
62
+ finally {
63
+ unlinkOwn();
64
+ unlinkScope();
65
+ }
66
+ }
package/dist/cjs/index.js CHANGED
@@ -21,10 +21,10 @@
21
21
  * If a symbol does not appear here, it is NOT part of the public API.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.commonsDeliveryPlatform = exports.pushTargetsFromDelivery = exports.selectCommonsDelivery = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = exports.OxyAppDataIdentifierError = exports.AccountNotOnDeviceError = exports.REPUTATION_CACHE_PREFIX = exports.StoreInstallerError = exports.setStoreInstaller = exports.getStoreInstaller = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_CATEGORY_IDS = exports.buildUserDid = exports.DirectUploadInterruptedError = exports.MAX_UPLOAD_BYTES = exports.assetUrlCacheTTL = exports.ASSET_MULTIPART_UPLOAD_MAX_BYTES = exports.sameHolderIdentity = exports.ServiceAssetMetadataError = exports.AssetUrlResolutionError = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyApiError = exports.OXY_CLOUD_URL = exports.OXY_API_URL = exports.OxyServices = void 0;
25
- exports.loadLocale = exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.coerceToSupportedLocale = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = void 0;
26
- exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.accountNameInvalidKey = exports.accountNameInvalidMessage = exports.accountNameMaxLength = exports.isValidAccountName = exports.isValidDisplayName = exports.isValidPassword = exports.isValidEmail = exports.ACCOUNT_TITLE_INVALID_MESSAGE = exports.DISPLAY_NAME_INVALID_MESSAGE = exports.MAX_DISPLAY_NAME_LENGTH = exports.PASSWORD_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.parseHttpErrorBody = exports.isHttpRequestError = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.buildQueryParams = exports.trustTierLabel = exports.reputationCategoryLabel = exports.accountRoleLabel = exports.accountCategoryLabel = exports.getLocalesVersion = exports.subscribeLocales = exports.isLocaleLoaded = void 0;
27
- exports.packageInfo = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.CENTRAL_IDP_APEX = exports.AUTH_WEB_ORIGIN = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = void 0;
24
+ exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.commonsDeliveryPlatform = exports.pushTargetsFromDelivery = exports.selectCommonsDelivery = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = exports.OxyAppDataIdentifierError = exports.AccountNotOnDeviceError = exports.REPUTATION_CACHE_PREFIX = exports.StoreInstallerError = exports.setStoreInstaller = exports.getStoreInstaller = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_CATEGORY_IDS = exports.buildUserDid = exports.DirectUploadInterruptedError = exports.MAX_UPLOAD_BYTES = exports.assetUrlCacheTTL = exports.ASSET_MULTIPART_UPLOAD_MAX_BYTES = exports.sameHolderIdentity = exports.retryAfterSecondsOf = exports.ServiceAssetMetadataError = exports.AssetUrlResolutionError = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyApiError = exports.OXY_CLOUD_URL = exports.OXY_API_URL = exports.OxyServices = void 0;
25
+ exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.coerceToSupportedLocale = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = void 0;
26
+ exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.accountNameInvalidKey = exports.accountNameInvalidMessage = exports.accountNameMaxLength = exports.isValidAccountName = exports.isValidDisplayName = exports.isValidPassword = exports.isValidEmail = exports.ACCOUNT_TITLE_INVALID_MESSAGE = exports.DISPLAY_NAME_INVALID_MESSAGE = exports.MAX_DISPLAY_NAME_LENGTH = exports.PASSWORD_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.parseHttpErrorBody = exports.isHttpRequestError = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.buildQueryParams = exports.trustTierLabel = exports.reputationCategoryLabel = exports.accountRoleLabel = exports.accountCategoryLabel = exports.getLocalesVersion = exports.subscribeLocales = exports.isLocaleLoaded = exports.loadLocale = void 0;
27
+ exports.packageInfo = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.CENTRAL_IDP_APEX = exports.AUTH_WEB_ORIGIN = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = void 0;
28
28
  // The crypto shim MUST stay the first import. The root itself ships no crypto
29
29
  // library (`__tests__/rootEntryGraph.test.ts`), but `@oxy.so/protocol`'s root
30
30
  // entry (reached by the OAuth/PKCE and device helpers) does, and Metro does not
@@ -44,6 +44,7 @@ Object.defineProperty(exports, "OxyAuthenticationError", { enumerable: true, get
44
44
  Object.defineProperty(exports, "OxyAuthenticationTimeoutError", { enumerable: true, get: function () { return OxyServices_errors_1.OxyAuthenticationTimeoutError; } });
45
45
  Object.defineProperty(exports, "AssetUrlResolutionError", { enumerable: true, get: function () { return OxyServices_errors_1.AssetUrlResolutionError; } });
46
46
  Object.defineProperty(exports, "ServiceAssetMetadataError", { enumerable: true, get: function () { return OxyServices_errors_1.ServiceAssetMetadataError; } });
47
+ Object.defineProperty(exports, "retryAfterSecondsOf", { enumerable: true, get: function () { return OxyServices_errors_1.retryAfterSecondsOf; } });
47
48
  var holderEvidence_1 = require("./api/holderEvidence");
48
49
  Object.defineProperty(exports, "sameHolderIdentity", { enumerable: true, get: function () { return holderEvidence_1.sameHolderIdentity; } });
49
50
  var assets_1 = require("./api/assets");
@@ -325,59 +325,42 @@ class OxyServer extends OxyServices_1.OxyServices {
325
325
  throw new Error('Requester context requires an authenticated service');
326
326
  return new requesterClient_1.OxyRequesterClient(this.baseURL, this.cloudURL, service, proof);
327
327
  }
328
+ // ── Server-side namespaces ───────────────────────────────────────────────
328
329
  /** The account graph, plus the service-only `accessFor` membership verdict. */
329
330
  get accounts() {
330
- if (!this._serverAccounts)
331
- this._serverAccounts = new namespaces_1.ServerAccountsApi(this.context);
332
- return this._serverAccounts;
331
+ return this.namespace('accounts', (context) => new namespaces_1.ServerAccountsApi(context));
333
332
  }
334
333
  /** Files, plus the service-only metadata and linked-URL lookups. */
335
334
  get assets() {
336
- if (!this._serverAssets)
337
- this._serverAssets = new namespaces_1.ServerAssetsApi(this.context);
338
- return this._serverAssets;
335
+ return this.namespace('assets', (context) => new namespaces_1.ServerAssetsApi(context));
339
336
  }
340
337
  /** The inbox and push tokens, plus `create`. */
341
338
  get notifications() {
342
- if (!this._serverNotifications)
343
- this._serverNotifications = new namespaces_1.ServerNotificationsApi(this.context);
344
- return this._serverNotifications;
339
+ return this.namespace('notifications', (context) => new namespaces_1.ServerNotificationsApi(context));
345
340
  }
346
341
  /** Linked external accounts, plus `forUser`. */
347
342
  get linkedAccounts() {
348
- if (!this._serverLinkedAccounts)
349
- this._serverLinkedAccounts = new namespaces_1.ServerLinkedAccountsApi(this.context);
350
- return this._serverLinkedAccounts;
343
+ return this.namespace('linkedAccounts', (context) => new namespaces_1.ServerLinkedAccountsApi(context));
351
344
  }
352
345
  /** Delegated capabilities, with the service-only authority calls. */
353
346
  get agency() {
354
- if (!this._serverAgency)
355
- this._serverAgency = new namespaces_1.ServerAgencyApi(this.context);
356
- return this._serverAgency;
347
+ return this.namespace('agency', (context) => new namespaces_1.ServerAgencyApi(context));
357
348
  }
358
349
  /** Application management, plus the receiver-bound Alia machine-key lookup. */
359
350
  get apps() {
360
- if (!this._serverApps)
361
- this._serverApps = new namespaces_1.ServerAppsApi(this.context);
362
- return this._serverApps;
351
+ return this.namespace('apps', (context) => new namespaces_1.ServerAppsApi(context));
363
352
  }
364
353
  /** Reputation reads, plus `award`. */
365
354
  get reputation() {
366
- if (!this._serverReputation)
367
- this._serverReputation = new namespaces_1.ServerReputationApi(this.context);
368
- return this._serverReputation;
355
+ return this.namespace('reputation', (context) => new namespaces_1.ServerReputationApi(context));
369
356
  }
370
357
  /** Policy reads, plus the consumer-service decision lanes (ADR 0041). */
371
358
  get safety() {
372
- if (!this._serverSafety)
373
- this._serverSafety = new safety_1.ServerSafetyApi(this.context);
374
- return this._serverSafety;
359
+ return this.namespace('safety', (context) => new safety_1.ServerSafetyApi(context));
375
360
  }
376
361
  /** Session state, plus validation through a reviewed supervised action. */
377
362
  get session() {
378
- if (!this._serverSession)
379
- this._serverSession = new session_1.ServerSessionApi(this.context, this.sessionValidationTtlMs, () => this.revocationFeedEpoch());
380
- return this._serverSession;
363
+ return this.namespace('session', (context) => new session_1.ServerSessionApi(context, this.sessionValidationTtlMs, () => this.revocationFeedEpoch()));
381
364
  }
382
365
  /** Close the revocation feed along with the HTTP client. */
383
366
  dispose() {
@@ -9,6 +9,7 @@ const externalMediaRequester_1 = require("./externalMediaRequester");
9
9
  * `oxy.assets`.
10
10
  */
11
11
  const contracts_1 = require("@oxy.so/contracts");
12
+ const callOptions_1 = require("../client/callOptions");
12
13
  const requesterContext_1 = require("./requesterContext");
13
14
  const assets_1 = require("../api/assets");
14
15
  const largeUpload_1 = require("./largeUpload");
@@ -33,6 +34,14 @@ const SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE = 100;
33
34
  const SERVICE_LINKED_DOWNLOAD_URL_CHUNK_SIZE = 25;
34
35
  /** Server-side cap of `POST`/`DELETE /assets/service/references`. */
35
36
  const SERVICE_REFERENCES_CHUNK_SIZE = 100;
37
+ /** Server-side cap of `POST /assets/service/federation/delete`. */
38
+ const SERVICE_FEDERATED_DELETE_CHUNK_SIZE = 20;
39
+ const FEDERATED_DELETE_RESULTS = new Set([
40
+ 'deleted',
41
+ 'not_found',
42
+ 'in_use',
43
+ 'forbidden',
44
+ ]);
36
45
  /** Server-side cap of `POST /assets/service/app/import/batch`. */
37
46
  const SERVICE_IMPORT_BATCH_SIZE = 50;
38
47
  /** An import fetches a remote source (up to 60 s server-side): a longer per-attempt timeout. */
@@ -61,14 +70,6 @@ const SERVICE_RECONCILE_RATE_LIMIT_MAX_WAIT_MS = 60000;
61
70
  const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
62
71
  /** A header value fetch accepts on every runtime: printable ASCII only. */
63
72
  const HEADER_SAFE_PATTERN = /^[\x20-\x7e]*$/;
64
- /** The `RequestOptions` half of {@link ServiceCallOptions}, only the fields that were set. */
65
- function serviceCallRequestOptions(options) {
66
- return {
67
- ...(options.signal ? { signal: options.signal } : {}),
68
- ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
69
- ...(options.retry !== undefined ? { retry: options.retry } : {}),
70
- };
71
- }
72
73
  /** The headers both streamed service uploads share. */
73
74
  function uploadHeaders(mime, description) {
74
75
  return {
@@ -185,6 +186,80 @@ class ServerAssetsApi extends assets_1.AssetsApi {
185
186
  */
186
187
  reconcile: (entityType, items, options = {}) => this.reconcileReferences(entityType, items, options),
187
188
  };
189
+ /**
190
+ * The federation media cache (`/assets/service/cache`): copies of remote
191
+ * media this application serves from Oxy's CDN instead of the remote host,
192
+ * in a reserved namespace that only holds what may be evicted. Every file
193
+ * is public by construction (`https://cloud.oxy.so/<id>`, `publicUrl`).
194
+ *
195
+ * - `upload` (`files:write`): the bytes as the whole body — a Blob from
196
+ * `fs.openAsBlob(path)` streams a file from disk; image, video or audio
197
+ * only (415), at most Oxy's cache size (413). Sent once.
198
+ * - `delete` (`federation:write`): evict one cache object. 404 when it is
199
+ * gone, 403 for anything outside the cache namespace — a service token can
200
+ * never delete media a person owns.
201
+ *
202
+ * A refusal for this application's media-write budget is a 429 whose
203
+ * `retryAfterSeconds` ({@link retryAfterSecondsOf}) says when to write again.
204
+ */
205
+ this.mediaCache = {
206
+ upload: (bytes, mime, options = {}) => lane(this.ctx).request('POST', '/assets/service/cache', bytes, {
207
+ headers: uploadHeaders(mime, options),
208
+ cache: false,
209
+ retry: false,
210
+ ...(options.signal ? { signal: options.signal } : {}),
211
+ ...(options.timeout ? { timeout: options.timeout } : {}),
212
+ }),
213
+ delete: async (fileId) => {
214
+ await lane(this.ctx).request('DELETE', `/assets/service/cache/${encodeURIComponent(fileId)}`, undefined, { cache: false });
215
+ },
216
+ };
217
+ /**
218
+ * Durable federated media (`/assets/service/federation`): a remote post's
219
+ * media re-hosted as a normal public file owned by the federated Oxy user
220
+ * it came from — outside the cache namespace, so a stored post may
221
+ * reference it for good.
222
+ *
223
+ * - `upload` (`files:write`): `ownerUserId` must be an existing federated
224
+ * user (403). Idempotent per (owner, application, bytes): the existing
225
+ * file with `deduplicated: true`, which other posts may share — check
226
+ * your own references before deleting it. Bytes another owner or
227
+ * application holds are a 409 `FEDERATED_MEDIA_OWNED_ELSEWHERE`. Sent once.
228
+ * - `delete` (`files:write` + `federation:write`): one answer per distinct
229
+ * id, in order, sent 20 at a time — only federated media this application
230
+ * uploaded is ever deleted (`forbidden` otherwise), and bytes another
231
+ * account holds are kept (`in_use`). Every step is idempotent, so a failed
232
+ * call is retried whole.
233
+ *
234
+ * Budget refusals are 429s, as for {@link mediaCache}.
235
+ */
236
+ this.federatedMedia = {
237
+ upload: async (ownerUserId, bytes, mime, options = {}) => {
238
+ const res = await lane(this.ctx).request('POST', '/assets/service/federation', bytes, {
239
+ headers: { ...uploadHeaders(mime, options), 'x-owner-user-id': ownerUserId },
240
+ cache: false,
241
+ retry: false,
242
+ ...(options.signal ? { signal: options.signal } : {}),
243
+ ...(options.timeout ? { timeout: options.timeout } : {}),
244
+ });
245
+ return { file: res.file, deduplicated: res.deduplicated === true };
246
+ },
247
+ delete: async (fileIds) => {
248
+ const results = [];
249
+ for (const part of chunk(uniqueIds(fileIds), SERVICE_FEDERATED_DELETE_CHUNK_SIZE)) {
250
+ const res = await lane(this.ctx).request('POST', '/assets/service/federation/delete', { ids: part }, { cache: false });
251
+ const asked = new Set(part);
252
+ for (const entry of Array.isArray(res?.results) ? res.results : []) {
253
+ // An id not asked, or a result this build does not know, is no
254
+ // answer: the caller sees the id unanswered and retries it.
255
+ if (asked.has(entry?.id) && FEDERATED_DELETE_RESULTS.has(entry?.result)) {
256
+ results.push({ id: entry.id, result: entry.result });
257
+ }
258
+ }
259
+ }
260
+ return results;
261
+ },
262
+ };
188
263
  /**
189
264
  * Per-play, revocable streaming access (ADR 0043 stage 4c). A play session
190
265
  * is ONE file's HLS ladder (a video's, or an audio file's audio-only one)
@@ -330,7 +405,7 @@ class ServerAssetsApi extends assets_1.AssetsApi {
330
405
  const chunk = references
331
406
  .slice(start, start + SERVICE_REFERENCES_CHUNK_SIZE)
332
407
  .map(({ fileId, entityType, entityId }) => ({ fileId, entityType, entityId }));
333
- const res = await lane(this.ctx).request(method, '/assets/service/references', { references: chunk }, { ...serviceCallRequestOptions(options), cache: false });
408
+ const res = await (0, callOptions_1.sendWithCallOptions)({ cache: false }, options, (requestOptions) => lane(this.ctx).request(method, '/assets/service/references', { references: chunk }, requestOptions));
334
409
  results.push(...res.results);
335
410
  }
336
411
  return results;
@@ -1037,26 +1112,10 @@ async function waitingOutRateLimits(send) {
1037
1112
  }
1038
1113
  /** How long to wait after a refusal: what Oxy said, else the doubling fallback; capped. */
1039
1114
  function rateLimitWaitMs(error, refusals) {
1040
- const said = retryAfterSeconds(error);
1115
+ const said = (0, OxyServices_errors_1.retryAfterSecondsOf)(error);
1041
1116
  const wait = said !== undefined ? said * 1000 : SERVICE_RECONCILE_RATE_LIMIT_FALLBACK_MS * 2 ** refusals;
1042
1117
  return Math.min(wait, SERVICE_RECONCILE_RATE_LIMIT_MAX_WAIT_MS);
1043
1118
  }
1044
- /** `details.retryAfterSeconds` of a refusal (`RateLimitedError` on the server), if any. */
1045
- function retryAfterSeconds(error) {
1046
- if (!error || typeof error !== 'object')
1047
- return undefined;
1048
- const candidates = [
1049
- error.details,
1050
- error.response
1051
- ?.data?.details,
1052
- ];
1053
- for (const details of candidates) {
1054
- const seconds = details?.retryAfterSeconds;
1055
- if (typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0)
1056
- return seconds;
1057
- }
1058
- return undefined;
1059
- }
1060
1119
  async function waitForCommit(service, path, sessionId) {
1061
1120
  const deadline = Date.now() + SERVICE_RECONCILE_COMMIT_WAIT_MS;
1062
1121
  let interval = assetJobs_1.ASSET_JOB_POLL_INITIAL_MS;
@@ -15,9 +15,7 @@ class OxyRequesterClient extends OxyServices_1.OxyServices {
15
15
  return this.requesterLane.request(method, url, data, options);
16
16
  }
17
17
  get assets() {
18
- if (!this.requesterAssets)
19
- this.requesterAssets = new namespaces_1.ServerAssetsApi(this.context);
20
- return this.requesterAssets;
18
+ return this.namespace('assets', (context) => new namespaces_1.ServerAssetsApi(context));
21
19
  }
22
20
  }
23
21
  exports.OxyRequesterClient = OxyRequesterClient;