@oxy.so/core 7.17.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.
- package/dist/cjs/HttpService.js +2 -0
- package/dist/cjs/OxyServices.errors.js +21 -0
- package/dist/cjs/OxyServices.js +86 -75
- package/dist/cjs/client/callOptions.js +66 -0
- package/dist/cjs/index.js +5 -4
- package/dist/cjs/server/OxyServer.js +10 -27
- package/dist/cjs/server/callerAccess.js +20 -1
- package/dist/cjs/server/namespaces.js +85 -26
- package/dist/cjs/server/requesterClient.js +1 -3
- package/dist/cjs/server/safeFetch.js +43 -12
- package/dist/esm/HttpService.js +2 -1
- package/dist/esm/OxyServices.errors.js +20 -0
- package/dist/esm/OxyServices.js +86 -75
- package/dist/esm/client/callOptions.js +61 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server/OxyServer.js +10 -27
- package/dist/esm/server/callerAccess.js +20 -1
- package/dist/esm/server/namespaces.js +86 -27
- package/dist/esm/server/requesterClient.js +1 -3
- package/dist/esm/server/safeFetch.js +43 -12
- package/dist/types/HttpService.d.ts +2 -0
- package/dist/types/OxyServices.d.ts +38 -25
- package/dist/types/OxyServices.errors.d.ts +6 -0
- package/dist/types/client/callOptions.d.ts +45 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/models/interfaces.d.ts +23 -0
- package/dist/types/server/OxyServer.d.ts +0 -9
- package/dist/types/server/callerAccess.d.ts +14 -1
- package/dist/types/server/namespaces.d.ts +50 -13
- package/dist/types/server/requesterClient.d.ts +0 -1
- package/dist/types/server/safeFetch.d.ts +16 -0
- package/package.json +1 -1
- package/src/HttpService.ts +2 -1
- package/src/OxyServices.errors.ts +19 -0
- package/src/OxyServices.ts +129 -75
- package/src/client/callOptions.ts +88 -0
- package/src/index.ts +12 -1
- package/src/models/interfaces.ts +25 -0
- package/src/server/OxyServer.ts +15 -33
- package/src/server/callerAccess.ts +34 -2
- package/src/server/namespaces.ts +128 -43
- package/src/server/requesterClient.ts +1 -3
- package/src/server/safeFetch.ts +69 -11
|
@@ -6,8 +6,9 @@ import { type ExternalMcpDocumentProof, type ExternalMcpMediaProof } from './ext
|
|
|
6
6
|
* `oxy.assets`.
|
|
7
7
|
*/
|
|
8
8
|
import { type AgentAvailability, type FirstPartyTaskInput, type FirstPartyExecutionAuthorizationInput, type ForegroundExecutionAuthorizationInput, type ActorRef, type AppCapabilityCatalog, type AutonomyLevel, type CapabilityTicketClaims, type CapabilityTicketRequest, type GrantLimit, type PolicyDecision, type ResourceRef, type CreateOxyNotificationRequest, type AwardReputationInput, type ReputationTransaction, type ServiceLinkedAccountListResponse, type AccountAccessVerdict } from '@oxy.so/contracts';
|
|
9
|
+
import { type CallOptions } from '../client/callOptions';
|
|
9
10
|
import type { OxyContext } from '../client/context';
|
|
10
|
-
import type { ApplicationFile, ApplicationFileAdoption, AssetLiveStatus, ApplicationFileImport, ApplicationFileImportBatchResult, ApplicationFilePage, ApplicationFileUpload, AudioRenditionOptions, CreatedRecordingTarget, FileImportOptions, FileImportState, FileVisibility, Notification, PlaySession, PlaySessionState, PlaySessionOptions, RecordingTarget, RecordingTargetOptions, ServiceAssetMetadata, ServiceAssetMetadataBySha, ServiceFileReference, ServiceFileReferenceResult, ServiceLinkedDownloadUrl, ServiceReferenceInventoryItem, ServiceReferenceReconcileResult, ServiceUserMediaFile, ServiceUserMediaImport } from '../models/interfaces';
|
|
11
|
+
import type { ApplicationFile, ApplicationFileAdoption, AssetLiveStatus, ApplicationFileImport, ApplicationFileImportBatchResult, ApplicationFilePage, ApplicationFileUpload, AudioRenditionOptions, CreatedRecordingTarget, FileImportOptions, FileImportState, FileVisibility, Notification, PlaySession, PlaySessionState, PlaySessionOptions, RecordingTarget, RecordingTargetOptions, ServiceAssetMetadata, ServiceAssetMetadataBySha, ServiceFileReference, ServiceFileReferenceResult, ServiceLinkedDownloadUrl, ServiceReferenceInventoryItem, ServiceReferenceReconcileResult, ServiceUserMediaFile, ServiceUserMediaImport, ServiceMediaCacheUpload, ServiceFederatedMediaUpload, ServiceFederatedMediaDeletion } from '../models/interfaces';
|
|
11
12
|
import { type RequesterProof } from './requesterContext';
|
|
12
13
|
import { AssetsApi } from '../api/assets';
|
|
13
14
|
import { type LargeServiceUploadOptions, type LargeServiceUploadResult, type LargeUploadSource } from './largeUpload';
|
|
@@ -21,20 +22,13 @@ import { type AliaMachineCredentialIntrospection } from './aliaMachineCredential
|
|
|
21
22
|
import { type BatchLookup } from '../utils/batchLookup';
|
|
22
23
|
/** The bytes of a streamed service upload: sent as the whole request body. */
|
|
23
24
|
export type ServiceUploadBody = Uint8Array | ArrayBuffer | Blob;
|
|
24
|
-
/** What `uploadAppFile` and `uploadUserMedia` may say about the bytes besides their type. */
|
|
25
25
|
/**
|
|
26
|
-
* Per-call transport options for a service call
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
26
|
+
* Per-call transport options for a service call — the {@link CallOptions}
|
|
27
|
+
* `withCallOptions` takes, accepted directly by the methods an outbox calls
|
|
28
|
+
* (`references.add` / `remove`).
|
|
29
29
|
*/
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
signal?: AbortSignal;
|
|
33
|
-
/** Per-attempt timeout in ms. */
|
|
34
|
-
timeout?: number;
|
|
35
|
-
/** `false` sends the call once; a transient failure is the caller's to retry (an outbox). */
|
|
36
|
-
retry?: boolean;
|
|
37
|
-
}
|
|
30
|
+
export type ServiceCallOptions = CallOptions;
|
|
31
|
+
/** What `uploadAppFile` and `uploadUserMedia` may say about the bytes besides their type. */
|
|
38
32
|
export interface ServiceUploadDescription {
|
|
39
33
|
/**
|
|
40
34
|
* Stored as the file's original name. Sent as a header, so only printable
|
|
@@ -103,6 +97,49 @@ export declare class ServerAssetsApi extends AssetsApi {
|
|
|
103
97
|
dryRun?: boolean;
|
|
104
98
|
}) => Promise<ServiceReferenceReconcileResult>;
|
|
105
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* The federation media cache (`/assets/service/cache`): copies of remote
|
|
102
|
+
* media this application serves from Oxy's CDN instead of the remote host,
|
|
103
|
+
* in a reserved namespace that only holds what may be evicted. Every file
|
|
104
|
+
* is public by construction (`https://cloud.oxy.so/<id>`, `publicUrl`).
|
|
105
|
+
*
|
|
106
|
+
* - `upload` (`files:write`): the bytes as the whole body — a Blob from
|
|
107
|
+
* `fs.openAsBlob(path)` streams a file from disk; image, video or audio
|
|
108
|
+
* only (415), at most Oxy's cache size (413). Sent once.
|
|
109
|
+
* - `delete` (`federation:write`): evict one cache object. 404 when it is
|
|
110
|
+
* gone, 403 for anything outside the cache namespace — a service token can
|
|
111
|
+
* never delete media a person owns.
|
|
112
|
+
*
|
|
113
|
+
* A refusal for this application's media-write budget is a 429 whose
|
|
114
|
+
* `retryAfterSeconds` ({@link retryAfterSecondsOf}) says when to write again.
|
|
115
|
+
*/
|
|
116
|
+
readonly mediaCache: {
|
|
117
|
+
upload: (bytes: ServiceUploadBody, mime: string, options?: ServiceUploadDescription) => Promise<ServiceMediaCacheUpload>;
|
|
118
|
+
delete: (fileId: string) => Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Durable federated media (`/assets/service/federation`): a remote post's
|
|
122
|
+
* media re-hosted as a normal public file owned by the federated Oxy user
|
|
123
|
+
* it came from — outside the cache namespace, so a stored post may
|
|
124
|
+
* reference it for good.
|
|
125
|
+
*
|
|
126
|
+
* - `upload` (`files:write`): `ownerUserId` must be an existing federated
|
|
127
|
+
* user (403). Idempotent per (owner, application, bytes): the existing
|
|
128
|
+
* file with `deduplicated: true`, which other posts may share — check
|
|
129
|
+
* your own references before deleting it. Bytes another owner or
|
|
130
|
+
* application holds are a 409 `FEDERATED_MEDIA_OWNED_ELSEWHERE`. Sent once.
|
|
131
|
+
* - `delete` (`files:write` + `federation:write`): one answer per distinct
|
|
132
|
+
* id, in order, sent 20 at a time — only federated media this application
|
|
133
|
+
* uploaded is ever deleted (`forbidden` otherwise), and bytes another
|
|
134
|
+
* account holds are kept (`in_use`). Every step is idempotent, so a failed
|
|
135
|
+
* call is retried whole.
|
|
136
|
+
*
|
|
137
|
+
* Budget refusals are 429s, as for {@link mediaCache}.
|
|
138
|
+
*/
|
|
139
|
+
readonly federatedMedia: {
|
|
140
|
+
upload: (ownerUserId: string, bytes: ServiceUploadBody, mime: string, options?: ServiceUploadDescription) => Promise<ServiceFederatedMediaUpload>;
|
|
141
|
+
delete: (fileIds: string[]) => Promise<ServiceFederatedMediaDeletion[]>;
|
|
142
|
+
};
|
|
106
143
|
/**
|
|
107
144
|
* Per-play, revocable streaming access (ADR 0043 stage 4c). A play session
|
|
108
145
|
* is ONE file's HLS ladder (a video's, or an audio file's audio-only one)
|
|
@@ -6,7 +6,6 @@ import { type RequesterProof } from './requesterContext';
|
|
|
6
6
|
/** Oxy namespaces bound to one verified requester, without sharing mutable auth state. */
|
|
7
7
|
export declare class OxyRequesterClient extends OxyServices {
|
|
8
8
|
private readonly requesterLane;
|
|
9
|
-
private requesterAssets?;
|
|
10
9
|
constructor(baseURL: string, cloudURL: string, original: ServiceLane, proof: RequesterProof);
|
|
11
10
|
request<T>(method: HttpMethod, url: string, data?: unknown, options?: RequestOptions): Promise<ResponsePayload<T>>;
|
|
12
11
|
get assets(): ServerAssetsApi;
|
|
@@ -150,6 +150,22 @@ export interface SafeFetchOptions {
|
|
|
150
150
|
* destroyed.
|
|
151
151
|
*/
|
|
152
152
|
signal?: AbortSignal;
|
|
153
|
+
/**
|
|
154
|
+
* The check of this very URL the caller already made with
|
|
155
|
+
* {@link assertSafePublicUrl} (to refuse a URL before it reaches a cache, a
|
|
156
|
+
* queue or a database, say): the first hop connects to the address it
|
|
157
|
+
* validated instead of resolving the name again. Only a check
|
|
158
|
+
* `assertSafePublicUrl` returned for exactly `rawUrl` is accepted; anything
|
|
159
|
+
* else is a programming error and throws a `TypeError` before any request.
|
|
160
|
+
* Every redirect hop is still resolved and checked afresh.
|
|
161
|
+
*/
|
|
162
|
+
validated?: SsrfCheckOk;
|
|
163
|
+
/**
|
|
164
|
+
* Refuse ({@link SsrfRejection}) any hop that is not https — the first, and
|
|
165
|
+
* every redirect — so no request of the chain is sent in the clear. Without
|
|
166
|
+
* it, http and https are both allowed on every hop.
|
|
167
|
+
*/
|
|
168
|
+
requireHttps?: boolean;
|
|
153
169
|
}
|
|
154
170
|
/**
|
|
155
171
|
* The validated response returned by {@link safeFetch}: the first non-redirect
|
package/package.json
CHANGED
package/src/HttpService.ts
CHANGED
|
@@ -514,7 +514,8 @@ function isAbortLike(error: unknown): boolean {
|
|
|
514
514
|
);
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
|
|
517
|
+
/** @internal Shared with `client/callOptions.ts`; see the note above `isAbortLike`. */
|
|
518
|
+
export function linkAbort(parent: AbortSignal, child: AbortController): () => void {
|
|
518
519
|
if (parent.aborted) {
|
|
519
520
|
child.abort((parent as AbortSignal & { reason?: unknown }).reason);
|
|
520
521
|
return () => {
|
|
@@ -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`)
|
package/src/OxyServices.ts
CHANGED
|
@@ -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
|
-
|
|
220
|
-
private
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
return this._contentClaims;
|
|
362
|
+
return this.namespace('contentClaims', (context) => new ContentClaimsApi(context));
|
|
294
363
|
}
|
|
295
364
|
|
|
296
365
|
get organizations(): OrganizationsApi {
|
|
297
|
-
|
|
298
|
-
return this._organizations;
|
|
366
|
+
return this.namespace('organizations', (context) => new OrganizationsApi(context));
|
|
299
367
|
}
|
|
300
368
|
|
|
301
369
|
get accounts(): AccountsApi {
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
return this._devices;
|
|
406
|
+
return this.namespace('devices', (context) => new DevicesApi(context));
|
|
349
407
|
}
|
|
350
408
|
get topics(): TopicsApi {
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
return this._console;
|
|
417
|
+
return this.namespace('console', (context) => new ConsoleApi(context));
|
|
363
418
|
}
|
|
364
419
|
get contacts(): ContactsApi {
|
|
365
|
-
|
|
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 {
|
|
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,
|
package/src/models/interfaces.ts
CHANGED
|
@@ -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
|