@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.
- 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/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/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/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/namespaces.ts +128 -43
- package/src/server/requesterClient.ts +1 -3
- package/src/server/safeFetch.ts +69 -11
|
@@ -6,12 +6,13 @@ import { bindExternalMcpMediaLane, } from './externalMediaRequester.js';
|
|
|
6
6
|
* `oxy.assets`.
|
|
7
7
|
*/
|
|
8
8
|
import { agentAvailabilitySchema, capabilityTicketRequestSchema, foregroundExecutionAuthorizationInputSchema, firstPartyExecutionAuthorizationInputSchema, firstPartyTaskInputSchema, accountAccessVerdictSchema, } from '@oxy.so/contracts';
|
|
9
|
+
import { sendWithCallOptions } from '../client/callOptions.js';
|
|
9
10
|
import { bindRequesterLane } from './requesterContext.js';
|
|
10
11
|
import { AssetsApi } from '../api/assets.js';
|
|
11
12
|
import { runLargeUpload, } from './largeUpload.js';
|
|
12
13
|
import { ASSET_JOB_POLL_INITIAL_MS, isPendingImport, nextPollInterval, sleep, waitForImport, } from './assetJobs.js';
|
|
13
14
|
import { NotificationsApi } from '../api/notifications.js';
|
|
14
|
-
import { OxyApiError } from '../OxyServices.errors.js';
|
|
15
|
+
import { OxyApiError, retryAfterSecondsOf } from '../OxyServices.errors.js';
|
|
15
16
|
import { LinkedAccountsApi } from '../api/linkedAccounts.js';
|
|
16
17
|
import { AgencyApi, } from '../api/agency.js';
|
|
17
18
|
import { ReputationApi } from '../api/reputation.js';
|
|
@@ -30,6 +31,14 @@ const SERVICE_ASSET_METADATA_BY_SHA_CHUNK_SIZE = 100;
|
|
|
30
31
|
const SERVICE_LINKED_DOWNLOAD_URL_CHUNK_SIZE = 25;
|
|
31
32
|
/** Server-side cap of `POST`/`DELETE /assets/service/references`. */
|
|
32
33
|
const SERVICE_REFERENCES_CHUNK_SIZE = 100;
|
|
34
|
+
/** Server-side cap of `POST /assets/service/federation/delete`. */
|
|
35
|
+
const SERVICE_FEDERATED_DELETE_CHUNK_SIZE = 20;
|
|
36
|
+
const FEDERATED_DELETE_RESULTS = new Set([
|
|
37
|
+
'deleted',
|
|
38
|
+
'not_found',
|
|
39
|
+
'in_use',
|
|
40
|
+
'forbidden',
|
|
41
|
+
]);
|
|
33
42
|
/** Server-side cap of `POST /assets/service/app/import/batch`. */
|
|
34
43
|
const SERVICE_IMPORT_BATCH_SIZE = 50;
|
|
35
44
|
/** An import fetches a remote source (up to 60 s server-side): a longer per-attempt timeout. */
|
|
@@ -58,14 +67,6 @@ const SERVICE_RECONCILE_RATE_LIMIT_MAX_WAIT_MS = 60000;
|
|
|
58
67
|
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
59
68
|
/** A header value fetch accepts on every runtime: printable ASCII only. */
|
|
60
69
|
const HEADER_SAFE_PATTERN = /^[\x20-\x7e]*$/;
|
|
61
|
-
/** The `RequestOptions` half of {@link ServiceCallOptions}, only the fields that were set. */
|
|
62
|
-
function serviceCallRequestOptions(options) {
|
|
63
|
-
return {
|
|
64
|
-
...(options.signal ? { signal: options.signal } : {}),
|
|
65
|
-
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
|
|
66
|
-
...(options.retry !== undefined ? { retry: options.retry } : {}),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
70
|
/** The headers both streamed service uploads share. */
|
|
70
71
|
function uploadHeaders(mime, description) {
|
|
71
72
|
return {
|
|
@@ -181,6 +182,80 @@ export class ServerAssetsApi extends AssetsApi {
|
|
|
181
182
|
*/
|
|
182
183
|
reconcile: (entityType, items, options = {}) => this.reconcileReferences(entityType, items, options),
|
|
183
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* The federation media cache (`/assets/service/cache`): copies of remote
|
|
187
|
+
* media this application serves from Oxy's CDN instead of the remote host,
|
|
188
|
+
* in a reserved namespace that only holds what may be evicted. Every file
|
|
189
|
+
* is public by construction (`https://cloud.oxy.so/<id>`, `publicUrl`).
|
|
190
|
+
*
|
|
191
|
+
* - `upload` (`files:write`): the bytes as the whole body — a Blob from
|
|
192
|
+
* `fs.openAsBlob(path)` streams a file from disk; image, video or audio
|
|
193
|
+
* only (415), at most Oxy's cache size (413). Sent once.
|
|
194
|
+
* - `delete` (`federation:write`): evict one cache object. 404 when it is
|
|
195
|
+
* gone, 403 for anything outside the cache namespace — a service token can
|
|
196
|
+
* never delete media a person owns.
|
|
197
|
+
*
|
|
198
|
+
* A refusal for this application's media-write budget is a 429 whose
|
|
199
|
+
* `retryAfterSeconds` ({@link retryAfterSecondsOf}) says when to write again.
|
|
200
|
+
*/
|
|
201
|
+
this.mediaCache = {
|
|
202
|
+
upload: (bytes, mime, options = {}) => lane(this.ctx).request('POST', '/assets/service/cache', bytes, {
|
|
203
|
+
headers: uploadHeaders(mime, options),
|
|
204
|
+
cache: false,
|
|
205
|
+
retry: false,
|
|
206
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
207
|
+
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
208
|
+
}),
|
|
209
|
+
delete: async (fileId) => {
|
|
210
|
+
await lane(this.ctx).request('DELETE', `/assets/service/cache/${encodeURIComponent(fileId)}`, undefined, { cache: false });
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Durable federated media (`/assets/service/federation`): a remote post's
|
|
215
|
+
* media re-hosted as a normal public file owned by the federated Oxy user
|
|
216
|
+
* it came from — outside the cache namespace, so a stored post may
|
|
217
|
+
* reference it for good.
|
|
218
|
+
*
|
|
219
|
+
* - `upload` (`files:write`): `ownerUserId` must be an existing federated
|
|
220
|
+
* user (403). Idempotent per (owner, application, bytes): the existing
|
|
221
|
+
* file with `deduplicated: true`, which other posts may share — check
|
|
222
|
+
* your own references before deleting it. Bytes another owner or
|
|
223
|
+
* application holds are a 409 `FEDERATED_MEDIA_OWNED_ELSEWHERE`. Sent once.
|
|
224
|
+
* - `delete` (`files:write` + `federation:write`): one answer per distinct
|
|
225
|
+
* id, in order, sent 20 at a time — only federated media this application
|
|
226
|
+
* uploaded is ever deleted (`forbidden` otherwise), and bytes another
|
|
227
|
+
* account holds are kept (`in_use`). Every step is idempotent, so a failed
|
|
228
|
+
* call is retried whole.
|
|
229
|
+
*
|
|
230
|
+
* Budget refusals are 429s, as for {@link mediaCache}.
|
|
231
|
+
*/
|
|
232
|
+
this.federatedMedia = {
|
|
233
|
+
upload: async (ownerUserId, bytes, mime, options = {}) => {
|
|
234
|
+
const res = await lane(this.ctx).request('POST', '/assets/service/federation', bytes, {
|
|
235
|
+
headers: { ...uploadHeaders(mime, options), 'x-owner-user-id': ownerUserId },
|
|
236
|
+
cache: false,
|
|
237
|
+
retry: false,
|
|
238
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
239
|
+
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
240
|
+
});
|
|
241
|
+
return { file: res.file, deduplicated: res.deduplicated === true };
|
|
242
|
+
},
|
|
243
|
+
delete: async (fileIds) => {
|
|
244
|
+
const results = [];
|
|
245
|
+
for (const part of chunk(uniqueIds(fileIds), SERVICE_FEDERATED_DELETE_CHUNK_SIZE)) {
|
|
246
|
+
const res = await lane(this.ctx).request('POST', '/assets/service/federation/delete', { ids: part }, { cache: false });
|
|
247
|
+
const asked = new Set(part);
|
|
248
|
+
for (const entry of Array.isArray(res?.results) ? res.results : []) {
|
|
249
|
+
// An id not asked, or a result this build does not know, is no
|
|
250
|
+
// answer: the caller sees the id unanswered and retries it.
|
|
251
|
+
if (asked.has(entry?.id) && FEDERATED_DELETE_RESULTS.has(entry?.result)) {
|
|
252
|
+
results.push({ id: entry.id, result: entry.result });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return results;
|
|
257
|
+
},
|
|
258
|
+
};
|
|
184
259
|
/**
|
|
185
260
|
* Per-play, revocable streaming access (ADR 0043 stage 4c). A play session
|
|
186
261
|
* is ONE file's HLS ladder (a video's, or an audio file's audio-only one)
|
|
@@ -326,7 +401,7 @@ export class ServerAssetsApi extends AssetsApi {
|
|
|
326
401
|
const chunk = references
|
|
327
402
|
.slice(start, start + SERVICE_REFERENCES_CHUNK_SIZE)
|
|
328
403
|
.map(({ fileId, entityType, entityId }) => ({ fileId, entityType, entityId }));
|
|
329
|
-
const res = await lane(this.ctx).request(method, '/assets/service/references', { references: chunk },
|
|
404
|
+
const res = await sendWithCallOptions({ cache: false }, options, (requestOptions) => lane(this.ctx).request(method, '/assets/service/references', { references: chunk }, requestOptions));
|
|
330
405
|
results.push(...res.results);
|
|
331
406
|
}
|
|
332
407
|
return results;
|
|
@@ -1027,26 +1102,10 @@ async function waitingOutRateLimits(send) {
|
|
|
1027
1102
|
}
|
|
1028
1103
|
/** How long to wait after a refusal: what Oxy said, else the doubling fallback; capped. */
|
|
1029
1104
|
function rateLimitWaitMs(error, refusals) {
|
|
1030
|
-
const said =
|
|
1105
|
+
const said = retryAfterSecondsOf(error);
|
|
1031
1106
|
const wait = said !== undefined ? said * 1000 : SERVICE_RECONCILE_RATE_LIMIT_FALLBACK_MS * 2 ** refusals;
|
|
1032
1107
|
return Math.min(wait, SERVICE_RECONCILE_RATE_LIMIT_MAX_WAIT_MS);
|
|
1033
1108
|
}
|
|
1034
|
-
/** `details.retryAfterSeconds` of a refusal (`RateLimitedError` on the server), if any. */
|
|
1035
|
-
function retryAfterSeconds(error) {
|
|
1036
|
-
if (!error || typeof error !== 'object')
|
|
1037
|
-
return undefined;
|
|
1038
|
-
const candidates = [
|
|
1039
|
-
error.details,
|
|
1040
|
-
error.response
|
|
1041
|
-
?.data?.details,
|
|
1042
|
-
];
|
|
1043
|
-
for (const details of candidates) {
|
|
1044
|
-
const seconds = details?.retryAfterSeconds;
|
|
1045
|
-
if (typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0)
|
|
1046
|
-
return seconds;
|
|
1047
|
-
}
|
|
1048
|
-
return undefined;
|
|
1049
|
-
}
|
|
1050
1109
|
async function waitForCommit(service, path, sessionId) {
|
|
1051
1110
|
const deadline = Date.now() + SERVICE_RECONCILE_COMMIT_WAIT_MS;
|
|
1052
1111
|
let interval = ASSET_JOB_POLL_INITIAL_MS;
|
|
@@ -12,8 +12,6 @@ export class OxyRequesterClient extends OxyServices {
|
|
|
12
12
|
return this.requesterLane.request(method, url, data, options);
|
|
13
13
|
}
|
|
14
14
|
get assets() {
|
|
15
|
-
|
|
16
|
-
this.requesterAssets = new ServerAssetsApi(this.context);
|
|
17
|
-
return this.requesterAssets;
|
|
15
|
+
return this.namespace('assets', (context) => new ServerAssetsApi(context));
|
|
18
16
|
}
|
|
19
17
|
}
|
|
@@ -129,6 +129,28 @@ const BLOCKED_IPV6_CIDRS = [
|
|
|
129
129
|
['2001:db8::', 32], // documentation
|
|
130
130
|
['64:ff9b::', 96], // NAT64 (maps to IPv4 — IPv4 denylist still applies after unwrap)
|
|
131
131
|
];
|
|
132
|
+
/**
|
|
133
|
+
* Every successful check {@link assertSafePublicUrl} returned, and the exact
|
|
134
|
+
* URL it checked. `safeFetch({ validated })` accepts only these, for that URL:
|
|
135
|
+
* a hand-built `{ ok: true, ip }`, or a check of another URL, never pins a
|
|
136
|
+
* connection.
|
|
137
|
+
*/
|
|
138
|
+
const issuedChecks = new WeakMap();
|
|
139
|
+
/** `rawUrl`'s normalized form, or `null` when it is not a URL. */
|
|
140
|
+
function normalizedUrl(rawUrl) {
|
|
141
|
+
try {
|
|
142
|
+
return new URL(rawUrl).href;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** A successful check of `href`, registered as one this module issued. */
|
|
149
|
+
function issueCheck(href, ip, family) {
|
|
150
|
+
const check = { ok: true, ip, family };
|
|
151
|
+
issuedChecks.set(check, href);
|
|
152
|
+
return check;
|
|
153
|
+
}
|
|
132
154
|
/**
|
|
133
155
|
* Return true if a literal IP address is private/loopback/link-local/reserved/
|
|
134
156
|
* multicast/metadata and therefore must NOT be contacted.
|
|
@@ -210,7 +232,7 @@ export async function assertSafePublicUrl(rawUrl, options = {}) {
|
|
|
210
232
|
if (isBlockedIp(literalHost)) {
|
|
211
233
|
return { ok: false, reason: 'literal ip in blocked range' };
|
|
212
234
|
}
|
|
213
|
-
return
|
|
235
|
+
return issueCheck(parsed.href, literalHost, literalFamily === 4 ? 4 : 6);
|
|
214
236
|
}
|
|
215
237
|
// Reject ambiguous numeric host forms BEFORE touching the resolver. `isIP`
|
|
216
238
|
// returned 0 (not a canonical IP literal), yet the host is made entirely of
|
|
@@ -251,11 +273,7 @@ export async function assertSafePublicUrl(rawUrl, options = {}) {
|
|
|
251
273
|
}
|
|
252
274
|
// All records are public. Pin the connection to the first one.
|
|
253
275
|
const chosen = records[0];
|
|
254
|
-
return
|
|
255
|
-
ok: true,
|
|
256
|
-
ip: chosen.address,
|
|
257
|
-
family: chosen.family === 4 ? 4 : 6,
|
|
258
|
-
};
|
|
276
|
+
return issueCheck(parsed.href, chosen.address, chosen.family === 4 ? 4 : 6);
|
|
259
277
|
}
|
|
260
278
|
/** What a refusal of a first-party Oxy host adds to its reason. */
|
|
261
279
|
const FIRST_PARTY_HOST_HINT = 'first-party Oxy host: inside the VPC it resolves to a private address, which safeFetch refuses by design; call it through OxyServer (its namespaces, serviceRequest, or createServiceClient().fetch), not safeFetch';
|
|
@@ -345,7 +363,13 @@ function fetchOnce(options, isHttps, headersTimeoutMs, body) {
|
|
|
345
363
|
* @throws {UpstreamError} on redirect-loop / malformed-redirect / timeout.
|
|
346
364
|
*/
|
|
347
365
|
export async function safeFetch(rawUrl, options = {}) {
|
|
348
|
-
const { method = 'GET', headers: callerHeaders, body, maxRedirects = MAX_REDIRECTS, redirect = 'follow', headersTimeoutMs = UPSTREAM_HEADERS_TIMEOUT_MS, signal, } = options;
|
|
366
|
+
const { method = 'GET', headers: callerHeaders, body, maxRedirects = MAX_REDIRECTS, redirect = 'follow', headersTimeoutMs = UPSTREAM_HEADERS_TIMEOUT_MS, signal, validated, requireHttps = false, } = options;
|
|
367
|
+
if (validated !== undefined) {
|
|
368
|
+
const checked = issuedChecks.get(validated);
|
|
369
|
+
if (checked === undefined || checked !== normalizedUrl(rawUrl)) {
|
|
370
|
+
throw new TypeError('safeFetch: `validated` must be the check assertSafePublicUrl returned for this exact URL');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
349
373
|
// Normalize a case-insensitive header map and ensure a User-Agent default.
|
|
350
374
|
const baseHeaders = {};
|
|
351
375
|
if (callerHeaders) {
|
|
@@ -362,11 +386,18 @@ export async function safeFetch(rawUrl, options = {}) {
|
|
|
362
386
|
if (signal?.aborted) {
|
|
363
387
|
throw new UpstreamError('request aborted');
|
|
364
388
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
389
|
+
if (requireHttps && !normalizedUrl(currentUrl)?.startsWith('https:')) {
|
|
390
|
+
throw new SsrfRejection(hop === 0 ? 'https required' : 'https required: redirect to a non-https url');
|
|
391
|
+
}
|
|
392
|
+
// Hop 0 may reuse the caller's check of this exact URL; a redirect hop
|
|
393
|
+
// targets a new URL and is always checked here. The lookup shares the
|
|
394
|
+
// headers deadline and the caller's signal.
|
|
395
|
+
const guard = hop === 0 && validated
|
|
396
|
+
? validated
|
|
397
|
+
: await assertSafePublicUrl(currentUrl, {
|
|
398
|
+
lookupTimeoutMs: headersTimeoutMs,
|
|
399
|
+
signal,
|
|
400
|
+
});
|
|
370
401
|
if (!guard.ok) {
|
|
371
402
|
throw guard.incomplete
|
|
372
403
|
? new UpstreamError(guard.reason)
|
|
@@ -169,6 +169,8 @@ type EnvelopeGuard<T> = T extends object ? 'data' extends DeclaredKeys<T> ? [Ext
|
|
|
169
169
|
* to carry the check to its own callers).
|
|
170
170
|
*/
|
|
171
171
|
export type ResponsePayload<T> = T & EnvelopeGuard<T>;
|
|
172
|
+
/** @internal Shared with `client/callOptions.ts`; see the note above `isAbortLike`. */
|
|
173
|
+
export declare function linkAbort(parent: AbortSignal, child: AbortController): () => void;
|
|
172
174
|
/**
|
|
173
175
|
* Unified HTTP Service
|
|
174
176
|
*
|
|
@@ -25,6 +25,7 @@ import { ContentClaimsApi } from './api/contentClaims';
|
|
|
25
25
|
import { HttpService, type RequestOptions, type ResponsePayload } from './HttpService';
|
|
26
26
|
import type { OxyConfig as OxyConfigBase } from './models/interfaces';
|
|
27
27
|
import type { HttpMethod, OxyContext } from './client/context';
|
|
28
|
+
import { type CallOptions } from './client/callOptions';
|
|
28
29
|
import { SessionApi } from './api/session';
|
|
29
30
|
import { AuthApi } from './api/auth';
|
|
30
31
|
import { UsersApi } from './api/users';
|
|
@@ -69,6 +70,16 @@ export interface LinkedHttpClient {
|
|
|
69
70
|
export declare const OXY_CLOUD_URL = "https://cloud.oxy.so";
|
|
70
71
|
/** Default Oxy API URL. */
|
|
71
72
|
export declare const OXY_API_URL = "https://api.oxy.so";
|
|
73
|
+
/**
|
|
74
|
+
* The namespaces a call scope ({@link OxyServices.withCallOptions}) carries:
|
|
75
|
+
* every one but `session`, which is the client's auth state rather than a call.
|
|
76
|
+
*/
|
|
77
|
+
export type OxyCallScopeNamespace = 'auth' | 'users' | 'follows' | 'privacy' | 'notifications' | 'assets' | 'identity' | 'safety' | 'contentClaims' | 'organizations' | 'accounts' | 'apps' | 'linkedAccounts' | 'agency' | 'store' | 'billing' | 'reputation' | 'civic' | 'nodes' | 'devices' | 'topics' | 'appData' | 'console' | 'contacts';
|
|
78
|
+
/**
|
|
79
|
+
* The namespaces of a client — an `OxyServer`'s server namespaces included —
|
|
80
|
+
* whose every request carries one set of {@link CallOptions}.
|
|
81
|
+
*/
|
|
82
|
+
export type OxyCallScope<T extends OxyServices = OxyServices> = Pick<T, OxyCallScopeNamespace>;
|
|
72
83
|
export declare class OxyServices {
|
|
73
84
|
/** The transport. Every namespace call goes through it. */
|
|
74
85
|
readonly http: HttpService;
|
|
@@ -119,31 +130,33 @@ export declare class OxyServices {
|
|
|
119
130
|
createLinkedClient(config: OxyConfig): LinkedHttpClient;
|
|
120
131
|
/** Release this client: its cache stops being swept and its session ends. */
|
|
121
132
|
dispose(): void;
|
|
122
|
-
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
/** Each namespace, created on first access and kept for the client's life. */
|
|
134
|
+
private readonly namespaceInstances;
|
|
135
|
+
/** The namespace `name`, created with this client's context on first access. */
|
|
136
|
+
protected namespace<T>(name: string, create: (context: OxyContext) => T): T;
|
|
137
|
+
/**
|
|
138
|
+
* This client's namespaces with per-call transport options: every request
|
|
139
|
+
* made through them carries `options`, on top of what each method sets.
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* // One slow write, one tight read — one client.
|
|
143
|
+
* await oxy.withCallOptions({ timeout: 60_000 }).assets.setVisibility(id, 'public');
|
|
144
|
+
* const page = await oxy.withCallOptions({ deadline: 2_500 }).users.byUsername(name);
|
|
145
|
+
* // A server's own namespaces too:
|
|
146
|
+
* await server.withCallOptions({ retry: false }).assets.references.add(refs);
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* - `timeout` / `deadline` replace the method's own and the client's.
|
|
150
|
+
* - `retry: false` sends each request once; `retry: true` never re-enables
|
|
151
|
+
* a request the method sends once on purpose.
|
|
152
|
+
* - `signal` aborts the call; a method's own signal still applies too.
|
|
153
|
+
*
|
|
154
|
+
* The scope is a view, not a client: it shares this client's session,
|
|
155
|
+
* cache, credentials and service lane, holds no resource and needs no
|
|
156
|
+
* disposal. A namespace one call makes into another (`privacy` reading
|
|
157
|
+
* `users.me()`) keeps the options. `session` is not part of it.
|
|
158
|
+
*/
|
|
159
|
+
withCallOptions(options: CallOptions): OxyCallScope<this>;
|
|
147
160
|
/** This client's auth state (token, user id) and its server session. */
|
|
148
161
|
get session(): SessionApi;
|
|
149
162
|
/** Signing in, signing up, and the account's credentials. */
|
|
@@ -142,6 +142,12 @@ export declare class OxyApiError extends Error {
|
|
|
142
142
|
cause?: unknown;
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* How long Oxy asked the caller to wait before trying again, in seconds — the
|
|
147
|
+
* `Retry-After` of a 429 (`details.retryAfterSeconds` of its body) — or
|
|
148
|
+
* `undefined` when the error is no such refusal or names no wait.
|
|
149
|
+
*/
|
|
150
|
+
export declare function retryAfterSecondsOf(error: unknown): number | undefined;
|
|
145
151
|
/**
|
|
146
152
|
* Normalise anything a request rejected with into an `OxyApiError`. An error
|
|
147
153
|
* that already is one (or a typed SDK error such as `OxyAuthenticationError`)
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { type RequestOptions } from '../HttpService';
|
|
10
|
+
/**
|
|
11
|
+
* Transport options for the calls made through `withCallOptions`, overriding
|
|
12
|
+
* the client's `requestTimeout` / `enableRetry` for those calls only.
|
|
13
|
+
*/
|
|
14
|
+
export interface CallOptions {
|
|
15
|
+
/** Abort the call. Combined with any signal the method passes itself. */
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
/** Per-attempt timeout in ms. Replaces the method's own default. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Wall-clock budget in ms for the whole call, retries and backoff included
|
|
21
|
+
* (`RequestOptions.deadline`) — "never more than 2.5 s" in one number.
|
|
22
|
+
*/
|
|
23
|
+
deadline?: number;
|
|
24
|
+
/**
|
|
25
|
+
* `false` sends each request once; a transient failure is the caller's to
|
|
26
|
+
* retry (an outbox). `true` allows the transport's retries where the client
|
|
27
|
+
* turned them off — but never on a request the method sends once on purpose
|
|
28
|
+
* (a non-idempotent write whose lost answer would duplicate it).
|
|
29
|
+
*/
|
|
30
|
+
retry?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Throws for a budget the transport cannot honour (0, negative, NaN, Infinity). */
|
|
33
|
+
export declare function assertCallOptions(options: CallOptions): void;
|
|
34
|
+
/**
|
|
35
|
+
* The request options a method chose, with the scope's applied on top. The
|
|
36
|
+
* signal is NOT merged here: two signals need a linked controller that lives
|
|
37
|
+
* exactly as long as the request ({@link sendWithCallOptions}).
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyCallOptions<T extends RequestOptions>(method: T | undefined, scope: CallOptions): T;
|
|
40
|
+
/**
|
|
41
|
+
* Send one request with the scope applied: `send` receives the merged
|
|
42
|
+
* options, with a signal that aborts when either the method's or the scope's
|
|
43
|
+
* does. The link between them is released when the request settles.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sendWithCallOptions<T, O extends RequestOptions>(method: O | undefined, scope: CallOptions, send: (options: O) => Promise<T>): Promise<T>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import './crypto/polyfill';
|
|
23
23
|
export { OxyServices, OXY_API_URL, OXY_CLOUD_URL } from './OxyServices';
|
|
24
|
-
export type { OxyConfig, LinkedHttpClient } from './OxyServices';
|
|
25
|
-
export {
|
|
24
|
+
export type { OxyConfig, LinkedHttpClient, OxyCallScope, OxyCallScopeNamespace, } from './OxyServices';
|
|
25
|
+
export type { CallOptions } from './client/callOptions';
|
|
26
|
+
export { OxyApiError, OxyAuthenticationError, OxyAuthenticationTimeoutError, AssetUrlResolutionError, ServiceAssetMetadataError, retryAfterSecondsOf, } from './OxyServices.errors';
|
|
26
27
|
export type { HttpMethod } from './client/context';
|
|
27
28
|
export type { RequestOptions, ResponsePayload, ResponseRequest, ResponseTransport, AuthenticatedResponseRequest, AuthRefreshReason, AuthRefreshHandler, CredentialGeneration, } from './HttpService';
|
|
28
29
|
export { sameHolderIdentity } from './api/holderEvidence';
|
|
@@ -75,7 +76,7 @@ export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken,
|
|
|
75
76
|
export type { HandleApiErrorOptions } from './utils/authHelpers';
|
|
76
77
|
export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from './utils/sessionUtils';
|
|
77
78
|
export type { ClientSession, StorageKeys, MinimalUserData, SessionLoginResponse, } from './models/session';
|
|
78
|
-
export type { PrivacySettings, NotificationPreferences, UserPreferences, User, LoginResponse, Notification, NotificationActor, NotificationPage, Wallet, Transaction, BlockedUser, RestrictedUser, TransferFundsRequest, PurchaseRequest, WithdrawalRequest, TransactionResponse, PaginationInfo, SearchProfilesResponse, ApiError, PaymentMethod, PaymentRequest, PaymentResponse, AnalyticsData, FollowerDetails, ContentViewer, FileMetadata, FileUploadResponse, FileListResponse, FileUpdateRequest, FileDeleteResponse, RNFileDescriptor, AssetUploadInput, FileVisibility, AssetLink, AssetMetadata, AssetVariant, Asset, AssetInitRequest, AssetInitResponse, AssetCompleteRequest, AssetLinkRequest, AssetUnlinkRequest, AssetUrlResponse, BatchFileAccessEntry, BatchFileAccessResponse, AssetDeleteSummary, AssetUpdateVisibilityRequest, AssetUpdateVisibilityResponse, ServiceAssetMetadata, AssetLiveStatus, AssetStatus, ServiceAssetMetadataBySha, ApplicationFile, ApplicationFileUpload, ApplicationFilePage, ServiceFileReference, ServiceFileReferenceResult, ServiceReferenceInventoryItem, ServiceReferenceReconcileResult, ServiceUserMediaFile, FileImportOutcome, FileImportSource, FileImportOptions, ApplicationFileImport, ApplicationFileImportBatchResult, ApplicationFileAdoption, ServiceUserMediaImport, AudioRenditionOptions, PendingFileImport, FileImportState, FileApplicationAccess, FileApplicationAccessRevocation, PlaySession, PlaySessionState, PlaySessionOptions, PlaySessionRendition, RecordingTarget, RecordingTargetOptions, RecordingTargetCredentials, CreatedRecordingTarget, AccountStorageCategoryUsage, AccountStorageUsageResponse, SecurityEventType, SecurityEventSeverity, SecurityActivity, SecurityActivityResponse, AssetUploadProgress, DeviceLinkedSession, DeviceLinkedSessionsResponse, DeviceLinkedSessionLogoutResponse, UpdateDeviceNameResponse, } from './models/interfaces';
|
|
79
|
+
export type { PrivacySettings, NotificationPreferences, UserPreferences, User, LoginResponse, Notification, NotificationActor, NotificationPage, Wallet, Transaction, BlockedUser, RestrictedUser, TransferFundsRequest, PurchaseRequest, WithdrawalRequest, TransactionResponse, PaginationInfo, SearchProfilesResponse, ApiError, PaymentMethod, PaymentRequest, PaymentResponse, AnalyticsData, FollowerDetails, ContentViewer, FileMetadata, FileUploadResponse, FileListResponse, FileUpdateRequest, FileDeleteResponse, RNFileDescriptor, AssetUploadInput, FileVisibility, AssetLink, AssetMetadata, AssetVariant, Asset, AssetInitRequest, AssetInitResponse, AssetCompleteRequest, AssetLinkRequest, AssetUnlinkRequest, AssetUrlResponse, BatchFileAccessEntry, BatchFileAccessResponse, AssetDeleteSummary, AssetUpdateVisibilityRequest, AssetUpdateVisibilityResponse, ServiceAssetMetadata, AssetLiveStatus, AssetStatus, ServiceAssetMetadataBySha, ApplicationFile, ApplicationFileUpload, ApplicationFilePage, ServiceFileReference, ServiceFileReferenceResult, ServiceReferenceInventoryItem, ServiceReferenceReconcileResult, ServiceUserMediaFile, ServiceMediaCacheUpload, ServiceFederatedMediaUpload, ServiceFederatedMediaDeleteResult, ServiceFederatedMediaDeletion, FileImportOutcome, FileImportSource, FileImportOptions, ApplicationFileImport, ApplicationFileImportBatchResult, ApplicationFileAdoption, ServiceUserMediaImport, AudioRenditionOptions, PendingFileImport, FileImportState, FileApplicationAccess, FileApplicationAccessRevocation, PlaySession, PlaySessionState, PlaySessionOptions, PlaySessionRendition, RecordingTarget, RecordingTargetOptions, RecordingTargetCredentials, CreatedRecordingTarget, AccountStorageCategoryUsage, AccountStorageUsageResponse, SecurityEventType, SecurityEventSeverity, SecurityActivity, SecurityActivityResponse, AssetUploadProgress, DeviceLinkedSession, DeviceLinkedSessionsResponse, DeviceLinkedSessionLogoutResponse, UpdateDeviceNameResponse, } from './models/interfaces';
|
|
79
80
|
export { SECURITY_EVENT_SEVERITY_MAP } from './models/interfaces';
|
|
80
81
|
export { TopicType, TopicSource } from './models/Topic';
|
|
81
82
|
export type { TopicData, TopicTranslation, TopicListResult } from './models/Topic';
|
|
@@ -967,6 +967,29 @@ export interface ServiceUserMediaFile {
|
|
|
967
967
|
mime: string;
|
|
968
968
|
visibility: FileVisibility;
|
|
969
969
|
}
|
|
970
|
+
/** `POST /assets/service/cache`: the file now in the federation media cache. */
|
|
971
|
+
export interface ServiceMediaCacheUpload {
|
|
972
|
+
file: {
|
|
973
|
+
id: string;
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/** `POST /assets/service/federation`: durable federated media, owned by the federated user. */
|
|
977
|
+
export interface ServiceFederatedMediaUpload {
|
|
978
|
+
file: ServiceUserMediaFile;
|
|
979
|
+
/** The owner already held these bytes from this application: the existing file, maybe shared. */
|
|
980
|
+
deduplicated: boolean;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Oxy's answer for one id of `assets.federatedMedia.delete`: `deleted` (this
|
|
984
|
+
* call removed it), `not_found` (nothing to delete), `in_use` (kept: another
|
|
985
|
+
* account holds the same bytes) or `forbidden` (not federated media this
|
|
986
|
+
* application uploaded; left untouched).
|
|
987
|
+
*/
|
|
988
|
+
export type ServiceFederatedMediaDeleteResult = 'deleted' | 'not_found' | 'in_use' | 'forbidden';
|
|
989
|
+
export interface ServiceFederatedMediaDeletion {
|
|
990
|
+
id: string;
|
|
991
|
+
result: ServiceFederatedMediaDeleteResult;
|
|
992
|
+
}
|
|
970
993
|
/**
|
|
971
994
|
* How Oxy renders an AUDIO file it stores (ADR 0043 stage 4c). Every audio
|
|
972
995
|
* file gets an audio-only HLS ladder (AAC 64k/128k/256k, a rung above the
|
|
@@ -206,15 +206,6 @@ export declare class OxyServer extends OxyServices {
|
|
|
206
206
|
private introspectResource;
|
|
207
207
|
/** An isolated Oxy client carrying the original session, task or operation proof. */
|
|
208
208
|
withRequester(proof: RequesterProof): OxyRequesterClient;
|
|
209
|
-
private _serverAccounts?;
|
|
210
|
-
private _serverAssets?;
|
|
211
|
-
private _serverNotifications?;
|
|
212
|
-
private _serverLinkedAccounts?;
|
|
213
|
-
private _serverAgency?;
|
|
214
|
-
private _serverApps?;
|
|
215
|
-
private _serverReputation?;
|
|
216
|
-
private _serverSafety?;
|
|
217
|
-
private _serverSession?;
|
|
218
209
|
/** The account graph, plus the service-only `accessFor` membership verdict. */
|
|
219
210
|
get accounts(): ServerAccountsApi;
|
|
220
211
|
/** Files, plus the service-only metadata and linked-URL lookups. */
|
|
@@ -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 () => {
|