@oxyhq/core 19.1.2 → 20.1.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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +23 -18
- package/dist/cjs/i18n/accountCategoryLabels.js +44 -0
- package/dist/cjs/i18n/accountRoleLabels.js +27 -0
- package/dist/cjs/i18n/reputationCategoryLabels.js +20 -0
- package/dist/cjs/i18n/trustTierLabels.js +19 -0
- package/dist/cjs/index.js +19 -9
- package/dist/cjs/mixins/OxyServices.chains.js +73 -0
- package/dist/cjs/mixins/OxyServices.followGraph.js +17 -0
- package/dist/cjs/mixins/OxyServices.store.js +266 -0
- package/dist/cjs/mixins/OxyServices.utility.js +159 -104
- package/dist/cjs/mixins/index.js +7 -0
- package/dist/cjs/server/rateLimit.js +15 -6
- package/dist/cjs/session/accountProjection.js +31 -6
- package/dist/cjs/utils/errorUtils.js +65 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +24 -19
- package/dist/esm/i18n/accountCategoryLabels.js +37 -0
- package/dist/esm/i18n/accountRoleLabels.js +20 -0
- package/dist/esm/i18n/reputationCategoryLabels.js +13 -0
- package/dist/esm/i18n/trustTierLabels.js +12 -0
- package/dist/esm/index.js +11 -8
- package/dist/esm/mixins/OxyServices.chains.js +70 -0
- package/dist/esm/mixins/OxyServices.followGraph.js +17 -0
- package/dist/esm/mixins/OxyServices.store.js +263 -0
- package/dist/esm/mixins/OxyServices.utility.js +159 -104
- package/dist/esm/mixins/index.js +7 -0
- package/dist/esm/server/rateLimit.js +15 -6
- package/dist/esm/session/accountProjection.js +30 -6
- package/dist/esm/utils/errorUtils.js +63 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/i18n/accountCategoryLabels.d.ts +34 -0
- package/dist/types/i18n/accountRoleLabels.d.ts +10 -0
- package/dist/types/i18n/reputationCategoryLabels.d.ts +10 -0
- package/dist/types/i18n/trustTierLabels.d.ts +9 -0
- package/dist/types/index.d.ts +14 -2
- package/dist/types/mixins/OxyServices.chains.d.ts +156 -0
- package/dist/types/mixins/OxyServices.followGraph.d.ts +13 -0
- package/dist/types/mixins/OxyServices.store.d.ts +334 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +31 -8
- package/dist/types/mixins/index.d.ts +3 -1
- package/dist/types/session/accountProjection.d.ts +20 -4
- package/dist/types/utils/errorUtils.d.ts +67 -0
- package/package.json +7 -6
- package/src/HttpService.ts +29 -22
- package/src/__tests__/parseHttpErrorBody.test.ts +116 -0
- package/src/__tests__/serverValueImportsDeclared.test.ts +7 -0
- package/src/i18n/__tests__/accountCategoryLabels.test.ts +62 -0
- package/src/i18n/__tests__/accountRoleLabels.test.ts +54 -0
- package/src/i18n/__tests__/reputationCategoryLabels.test.ts +56 -0
- package/src/i18n/__tests__/trustTierLabels.test.ts +47 -0
- package/src/i18n/accountCategoryLabels.ts +44 -0
- package/src/i18n/accountRoleLabels.ts +26 -0
- package/src/i18n/reputationCategoryLabels.ts +20 -0
- package/src/i18n/trustTierLabels.ts +18 -0
- package/src/index.ts +43 -6
- package/src/mixins/OxyServices.chains.ts +134 -0
- package/src/mixins/OxyServices.followGraph.ts +24 -0
- package/src/mixins/OxyServices.store.ts +585 -0
- package/src/mixins/OxyServices.utility.ts +161 -108
- package/src/mixins/__tests__/chains.test.ts +113 -0
- package/src/mixins/__tests__/followGraph.test.ts +19 -0
- package/src/mixins/__tests__/store.test.ts +304 -0
- package/src/mixins/__tests__/userTokenAuth.test.ts +746 -0
- package/src/mixins/index.ts +9 -0
- package/src/server/__tests__/rateLimit.test.ts +47 -0
- package/src/server/rateLimit.ts +18 -8
- package/src/session/__tests__/accountProjection.test.ts +98 -0
- package/src/session/accountProjection.ts +37 -6
- package/src/utils/errorUtils.ts +116 -5
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { TRUST_TIERS } from '@oxyhq/contracts';
|
|
2
|
+
import { EN_TRUST_TIER_LABELS, trustTierLabel } from '../trustTierLabels';
|
|
3
|
+
|
|
4
|
+
const SHIPPED_LOCALES = [
|
|
5
|
+
'en-US',
|
|
6
|
+
'es-ES',
|
|
7
|
+
'ca-ES',
|
|
8
|
+
'fr-FR',
|
|
9
|
+
'de-DE',
|
|
10
|
+
'it-IT',
|
|
11
|
+
'pt-PT',
|
|
12
|
+
'ja-JP',
|
|
13
|
+
'ko-KR',
|
|
14
|
+
'zh-CN',
|
|
15
|
+
'ar-SA',
|
|
16
|
+
] as const;
|
|
17
|
+
const REGION_VARIANT = 'es-MX';
|
|
18
|
+
const UNSHIPPED_LOCALE = 'nl-NL';
|
|
19
|
+
|
|
20
|
+
describe('trustTierLabel', () => {
|
|
21
|
+
it('covers the whole vocabulary', () => {
|
|
22
|
+
expect(TRUST_TIERS.length).toBe(5);
|
|
23
|
+
expect(Object.keys(EN_TRUST_TIER_LABELS)).toHaveLength(TRUST_TIERS.length);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it.each([...SHIPPED_LOCALES, REGION_VARIANT, UNSHIPPED_LOCALE])(
|
|
27
|
+
'names every tier in %s — never a key, never a slug, never empty',
|
|
28
|
+
(locale) => {
|
|
29
|
+
for (const tier of TRUST_TIERS) {
|
|
30
|
+
const label = trustTierLabel(locale, tier);
|
|
31
|
+
expect(label).not.toBe('');
|
|
32
|
+
expect(label).not.toBe(`trust.tiers.${tier}`);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
it('falls back to English for a language with no tier translations', () => {
|
|
38
|
+
expect(trustTierLabel('de-DE', 'high_trust')).toBe(
|
|
39
|
+
EN_TRUST_TIER_LABELS.high_trust,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('resolves a region variant through its base language', () => {
|
|
44
|
+
expect(trustTierLabel('es-MX', 'verified')).toBe(trustTierLabel('es-ES', 'verified'));
|
|
45
|
+
expect(trustTierLabel('es-MX', 'verified')).not.toBe(EN_TRUST_TIER_LABELS.verified);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AccountCategoryId } from '@oxyhq/contracts';
|
|
2
|
+
import enUS from './locales/en-US.json';
|
|
3
|
+
import { translate } from './index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every account category's English name, keyed by its stable id.
|
|
7
|
+
*
|
|
8
|
+
* **The annotation is the point.** The vocabulary lives in `@oxyhq/contracts`
|
|
9
|
+
* and the names live in `locales/en-US.json`, so they are two lists that must
|
|
10
|
+
* agree and nothing but a type can make them. Declaring the JSON node as a
|
|
11
|
+
* TOTAL `Record<AccountCategoryId, string>` turns "somebody added a category at
|
|
12
|
+
* Oxy and nobody wrote its English" into a `TS2741` naming the missing id, at
|
|
13
|
+
* build time, instead of a picker row that paints `accounts.accountCategory.<id>`
|
|
14
|
+
* at a user trying to choose one.
|
|
15
|
+
*
|
|
16
|
+
* That failure is not hypothetical. The screen previously wrote `t(key) || id`,
|
|
17
|
+
* whose author believed an unnamed id would degrade to its raw slug. It cannot:
|
|
18
|
+
* {@link translate} echoes the KEY when it resolves nothing, and a non-empty
|
|
19
|
+
* string is never falsy, so the `|| id` arm was unreachable and the output was
|
|
20
|
+
* the dotted key. A runtime fallback that cannot run is worse than none,
|
|
21
|
+
* because it reads as protection.
|
|
22
|
+
*
|
|
23
|
+
* Totality is over `ACCOUNT_CATEGORY_IDS`, which RETAINS withdrawn ids, so an
|
|
24
|
+
* account still carrying a retired category keeps rendering its name while no
|
|
25
|
+
* picker offers it again. Retired and unknown are different cases: only an id
|
|
26
|
+
* outside the union is unnameable, which is why this is keyed by
|
|
27
|
+
* `AccountCategoryId` and not by `string`.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Module-scoped, NOT re-exported from the package index: the annotation is the
|
|
31
|
+
* whole job, and it does that job without being public API. It carries no
|
|
32
|
+
* `Object.freeze` and no `Readonly<>` for the same reason — those existed only
|
|
33
|
+
* to make an exported reference safe from a consumer's stray write, and there
|
|
34
|
+
* is no such consumer. Exported from the MODULE so its own test can name it.
|
|
35
|
+
*/
|
|
36
|
+
export const EN_ACCOUNT_CATEGORY_LABELS: Record<AccountCategoryId, string> =
|
|
37
|
+
enUS.accounts.accountCategory;
|
|
38
|
+
|
|
39
|
+
export function accountCategoryLabel(
|
|
40
|
+
locale: string | undefined,
|
|
41
|
+
id: AccountCategoryId,
|
|
42
|
+
): string {
|
|
43
|
+
return translate(locale, `accounts.accountCategory.${id}`);
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AccountRole } from '../mixins/OxyServices.accounts';
|
|
2
|
+
import enUS from './locales/en-US.json';
|
|
3
|
+
import { translate } from './index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every account member role's English name, keyed by its stable id.
|
|
7
|
+
*
|
|
8
|
+
* Totality is over the closed `AccountRole` union so a new role without an
|
|
9
|
+
* English label is a build error, not a members row that paints
|
|
10
|
+
* `accounts.roles.<role>.label`.
|
|
11
|
+
*/
|
|
12
|
+
export const EN_ACCOUNT_ROLE_LABELS: Record<AccountRole, string> = {
|
|
13
|
+
owner: enUS.accounts.roles.owner.label,
|
|
14
|
+
admin: enUS.accounts.roles.admin.label,
|
|
15
|
+
editor: enUS.accounts.roles.editor.label,
|
|
16
|
+
developer: enUS.accounts.roles.developer.label,
|
|
17
|
+
billing: enUS.accounts.roles.billing.label,
|
|
18
|
+
viewer: enUS.accounts.roles.viewer.label,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function accountRoleLabel(
|
|
22
|
+
locale: string | undefined,
|
|
23
|
+
role: AccountRole,
|
|
24
|
+
): string {
|
|
25
|
+
return translate(locale, `accounts.roles.${role}.label`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReputationCategory } from '@oxyhq/contracts';
|
|
2
|
+
import enUS from './locales/en-US.json';
|
|
3
|
+
import { translate } from './index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every reputation rule category's English name, keyed by its stable id.
|
|
7
|
+
*
|
|
8
|
+
* Totality is over `REPUTATION_CATEGORIES` from `@oxyhq/contracts` so a new
|
|
9
|
+
* category added server-side without an English label is a build error, not a
|
|
10
|
+
* Trust Rules section title that paints `trust.rules.categories.<id>`.
|
|
11
|
+
*/
|
|
12
|
+
export const EN_REPUTATION_CATEGORY_LABELS: Record<ReputationCategory, string> =
|
|
13
|
+
enUS.trust.rules.categories;
|
|
14
|
+
|
|
15
|
+
export function reputationCategoryLabel(
|
|
16
|
+
locale: string | undefined,
|
|
17
|
+
id: ReputationCategory,
|
|
18
|
+
): string {
|
|
19
|
+
return translate(locale, `trust.rules.categories.${id}`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TrustTier } from '@oxyhq/contracts';
|
|
2
|
+
import enUS from './locales/en-US.json';
|
|
3
|
+
import { translate } from './index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every trust tier's English name, keyed by its stable id.
|
|
7
|
+
*
|
|
8
|
+
* Totality is over `TRUST_TIERS` from `@oxyhq/contracts` so a new tier without
|
|
9
|
+
* an English label is a build error, not a chip that paints `trust.tiers.<id>`.
|
|
10
|
+
*/
|
|
11
|
+
export const EN_TRUST_TIER_LABELS: Record<TrustTier, string> = enUS.trust.tiers;
|
|
12
|
+
|
|
13
|
+
export function trustTierLabel(
|
|
14
|
+
locale: string | undefined,
|
|
15
|
+
tier: TrustTier,
|
|
16
|
+
): string {
|
|
17
|
+
return translate(locale, `trust.tiers.${tier}`);
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -116,6 +116,29 @@ export type {
|
|
|
116
116
|
ConnectedApp,
|
|
117
117
|
} from './mixins/OxyServices.connectedApps';
|
|
118
118
|
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// App store (public storefront + reviews + the listing a publisher edits)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
export type {
|
|
123
|
+
StoreCategory,
|
|
124
|
+
StoreRating,
|
|
125
|
+
StoreListingSummary,
|
|
126
|
+
StoreListingDetail,
|
|
127
|
+
StoreScreenshot,
|
|
128
|
+
StoreScreenshotPlatform,
|
|
129
|
+
StoreReview,
|
|
130
|
+
StoreOwnReview,
|
|
131
|
+
WriteStoreReviewInput,
|
|
132
|
+
StoreListingStatus,
|
|
133
|
+
PublisherListing,
|
|
134
|
+
WriteListingInput,
|
|
135
|
+
AddScreenshotInput,
|
|
136
|
+
UpdateScreenshotInput,
|
|
137
|
+
StorePage,
|
|
138
|
+
StorePageOptions,
|
|
139
|
+
StoreReviewsOptions,
|
|
140
|
+
} from './mixins/OxyServices.store';
|
|
141
|
+
|
|
119
142
|
// ---------------------------------------------------------------------------
|
|
120
143
|
// Accounts (unified account graph: tree, membership, roles, bot credentials)
|
|
121
144
|
// plus the applications owned within it (Application = OAuth client).
|
|
@@ -230,6 +253,13 @@ export type {
|
|
|
230
253
|
} from './mixins/OxyServices.civic';
|
|
231
254
|
export type { UserNodeStatus, UserNodeMode, UserNodeController, UserNodeLivenessStatus, RegisterNodeInput, RemoveNodeResult } from './mixins/OxyServices.nodes';
|
|
232
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Chains — the shared per-person record log. `ChainRecord` is generic over the
|
|
258
|
+
* app's own lexicon payload, so a consumer types its records without Oxy
|
|
259
|
+
* knowing any app's schema.
|
|
260
|
+
*/
|
|
261
|
+
export type { ChainRecord, ChainRecordPage, AppendedChainRecord } from './mixins/OxyServices.chains';
|
|
262
|
+
|
|
233
263
|
// ---------------------------------------------------------------------------
|
|
234
264
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
235
265
|
// ---------------------------------------------------------------------------
|
|
@@ -453,6 +483,10 @@ export type { CircuitBreakerState, CircuitBreakerConfig } from './shared/utils/n
|
|
|
453
483
|
// i18n
|
|
454
484
|
// ---------------------------------------------------------------------------
|
|
455
485
|
export { translate } from './i18n';
|
|
486
|
+
export { accountCategoryLabel } from './i18n/accountCategoryLabels';
|
|
487
|
+
export { accountRoleLabel } from './i18n/accountRoleLabels';
|
|
488
|
+
export { reputationCategoryLabel } from './i18n/reputationCategoryLabels';
|
|
489
|
+
export { trustTierLabel } from './i18n/trustTierLabels';
|
|
456
490
|
|
|
457
491
|
// ---------------------------------------------------------------------------
|
|
458
492
|
// API request / URL helpers
|
|
@@ -476,8 +510,11 @@ export {
|
|
|
476
510
|
ErrorCodes,
|
|
477
511
|
createApiError,
|
|
478
512
|
handleHttpError,
|
|
513
|
+
isHttpRequestError,
|
|
514
|
+
parseHttpErrorBody,
|
|
479
515
|
validateRequiredFields,
|
|
480
516
|
} from './utils/errorUtils';
|
|
517
|
+
export type { HttpRequestError, ParsedHttpErrorBody } from './utils/errorUtils';
|
|
481
518
|
|
|
482
519
|
export { retryAsync } from './utils/asyncUtils';
|
|
483
520
|
|
|
@@ -647,14 +684,14 @@ export {
|
|
|
647
684
|
// chooser: device sign-ins ∪ account graph, deduped by accountId). Pure +
|
|
648
685
|
// I/O-free — the caller hydrates profiles via `getUsersByIds`. Shared by
|
|
649
686
|
// `@oxyhq/services` and auth.oxy.so so the list can't diverge.
|
|
650
|
-
// `isSwitchTargetAccount` is the
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
// too, so gating a switcher on it alone empties the list.
|
|
687
|
+
// `isSwitchTargetAccount` is the structural half ("is this kind switchable at
|
|
688
|
+
// all?"); `canSwitchIntoAccount` adds the caller's `account:act_as` permission.
|
|
689
|
+
// Both are exported so surfaces that render `AccountNode`s rather than the
|
|
690
|
+
// projection — the Console workspace switcher, managed-accounts rows — ask the
|
|
691
|
+
// SAME questions instead of testing a kind literal.
|
|
656
692
|
export {
|
|
657
693
|
isSwitchTargetAccount,
|
|
694
|
+
canSwitchIntoAccount,
|
|
658
695
|
projectSwitchableAccounts,
|
|
659
696
|
switchableAccountIds,
|
|
660
697
|
} from './session/accountProjection';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chains — the shared record log every Oxy app reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* A person has ONE chain. An app appends its own records to it and projects its
|
|
5
|
+
* feeds from what it reads back, instead of keeping a private copy of the same
|
|
6
|
+
* person's activity. This mixin is the client half of `/chains` in oxy-api, and
|
|
7
|
+
* it exists so that adopting the chain costs an app no HTTP of its own — the
|
|
8
|
+
* whole point of the shared substrate is that the second app writes less code
|
|
9
|
+
* than the first, not the same amount in a different file.
|
|
10
|
+
*
|
|
11
|
+
* ## Both calls are SERVICE-authenticated
|
|
12
|
+
*
|
|
13
|
+
* They go through `makeServiceRequest`, so they only work on a backend that has
|
|
14
|
+
* called `configureServiceAuth()`. That is not an accident of implementation: an
|
|
15
|
+
* append writes to someone else's chain and a read spans many subjects, so
|
|
16
|
+
* neither belongs in a browser holding a user session. A frontend that needs
|
|
17
|
+
* this asks its own backend.
|
|
18
|
+
*
|
|
19
|
+
* The authority is checked server-side and cannot be talked out of from here:
|
|
20
|
+
* `chains:write` plus the application's own `chainNamespaces` for an append,
|
|
21
|
+
* `chains:read` plus the public-collection policy for a read. A call that
|
|
22
|
+
* violates either gets a 403 or an empty page — this client adds no
|
|
23
|
+
* pre-validation that could drift from the server's answer.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
27
|
+
|
|
28
|
+
/** A signed record as it comes back from a read. */
|
|
29
|
+
export interface ChainRecord<TRecord = Record<string, unknown>> {
|
|
30
|
+
recordId: string;
|
|
31
|
+
/** The subject whose chain it is — the person the record is about. */
|
|
32
|
+
oxyUserId: string;
|
|
33
|
+
/** The lexicon NSID, e.g. `app.mention.feed.post`. */
|
|
34
|
+
collection: string;
|
|
35
|
+
envelope: {
|
|
36
|
+
version: number;
|
|
37
|
+
type: string;
|
|
38
|
+
subject: string;
|
|
39
|
+
issuer: string;
|
|
40
|
+
record: TRecord;
|
|
41
|
+
issuedAt: number;
|
|
42
|
+
seq?: number;
|
|
43
|
+
prev?: string | null;
|
|
44
|
+
collection?: string;
|
|
45
|
+
rkey?: string;
|
|
46
|
+
publicKey: string;
|
|
47
|
+
alg: string;
|
|
48
|
+
signature: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One page of a multi-subject read. */
|
|
53
|
+
export interface ChainRecordPage<TRecord = Record<string, unknown>> {
|
|
54
|
+
records: ChainRecord<TRecord>[];
|
|
55
|
+
/**
|
|
56
|
+
* Opaque. Hand it back as `since` to continue; `null` at the end of the
|
|
57
|
+
* stream as of this snapshot. Never construct one.
|
|
58
|
+
*/
|
|
59
|
+
nextCursor: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** What an append returns once the record is on the chain. */
|
|
63
|
+
export interface AppendedChainRecord {
|
|
64
|
+
recordId: string;
|
|
65
|
+
seq: number;
|
|
66
|
+
envelope: ChainRecord['envelope'];
|
|
67
|
+
verified: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function OxyServicesChainsMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
71
|
+
return class extends Base {
|
|
72
|
+
constructor(...args: any[]) {
|
|
73
|
+
super(...(args as [any]));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Service-token request, implemented by the auth mixin earlier in the pipeline. */
|
|
77
|
+
declare makeServiceRequest: <R = unknown>(
|
|
78
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
79
|
+
url: string,
|
|
80
|
+
data?: unknown,
|
|
81
|
+
userId?: string,
|
|
82
|
+
) => Promise<R>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Append a record to `oxyUserId`'s chain under `collection`/`rkey`.
|
|
86
|
+
*
|
|
87
|
+
* Oxy issues and signs it; the calling app never holds a chain signing key.
|
|
88
|
+
* `rkey` is the app's own id for the thing — reusing it later supersedes the
|
|
89
|
+
* earlier record for that key, which is how an edit works.
|
|
90
|
+
*
|
|
91
|
+
* Requires the `chains:write` scope AND `collection` falling under one of
|
|
92
|
+
* this application's granted `chainNamespaces`. Both are enforced by the
|
|
93
|
+
* server; a violation throws with a 403.
|
|
94
|
+
*/
|
|
95
|
+
async appendChainRecord(params: {
|
|
96
|
+
oxyUserId: string;
|
|
97
|
+
collection: string;
|
|
98
|
+
rkey: string;
|
|
99
|
+
record: Record<string, unknown>;
|
|
100
|
+
}): Promise<AppendedChainRecord> {
|
|
101
|
+
return this.makeServiceRequest<AppendedChainRecord>('POST', '/chains/records', params);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Records published by any of `oxyUserIds` under any of `collections`,
|
|
106
|
+
* oldest first — the read a cross-app feed is projected from.
|
|
107
|
+
*
|
|
108
|
+
* Only collections Oxy declares PUBLIC come back, whatever is asked for; a
|
|
109
|
+
* private one yields nothing rather than an error.
|
|
110
|
+
*
|
|
111
|
+
* **Re-poll from slightly BEFORE your last cursor and dedupe by
|
|
112
|
+
* `recordId`.** The chain's pagination axis is a transaction-start
|
|
113
|
+
* timestamp, so a record can commit behind a cursor that already passed it.
|
|
114
|
+
* Re-delivering one costs bytes; skipping one costs a record that never
|
|
115
|
+
* appears. Projections are expected to be idempotent for exactly this
|
|
116
|
+
* reason.
|
|
117
|
+
*/
|
|
118
|
+
async readChainRecords<TRecord = Record<string, unknown>>(params: {
|
|
119
|
+
oxyUserIds: readonly string[];
|
|
120
|
+
collections: readonly string[];
|
|
121
|
+
since?: string | null;
|
|
122
|
+
limit?: number;
|
|
123
|
+
}): Promise<ChainRecordPage<TRecord>> {
|
|
124
|
+
const query = new URLSearchParams({
|
|
125
|
+
authors: params.oxyUserIds.join(','),
|
|
126
|
+
collections: params.collections.join(','),
|
|
127
|
+
});
|
|
128
|
+
if (params.since) query.set('since', params.since);
|
|
129
|
+
if (params.limit !== undefined) query.set('limit', String(params.limit));
|
|
130
|
+
|
|
131
|
+
return this.makeServiceRequest<ChainRecordPage<TRecord>>('GET', `/chains/records?${query.toString()}`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -210,6 +210,30 @@ export function OxyServicesFollowGraphMixin<T extends typeof OxyServicesBase>(Ba
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Release a namespace the calling application holds, when nothing is
|
|
215
|
+
registered inside it yet.
|
|
216
|
+
*
|
|
217
|
+
* Idempotent when the namespace is already unowned (`released: false`).
|
|
218
|
+
* Exists because claims are first-come and registration runs on boot — a
|
|
219
|
+
* development build with the wrong client id can bind a name permanently
|
|
220
|
+
* unless the holder can give it back.
|
|
221
|
+
*/
|
|
222
|
+
async releaseFollowNamespace(
|
|
223
|
+
namespace: string,
|
|
224
|
+
): Promise<{ namespace: string; released: boolean }> {
|
|
225
|
+
try {
|
|
226
|
+
return await this.makeRequest(
|
|
227
|
+
'DELETE',
|
|
228
|
+
`/v2/follow-targets/namespaces/${encodeURIComponent(namespace)}`,
|
|
229
|
+
undefined,
|
|
230
|
+
{ cache: false },
|
|
231
|
+
);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw this.handleError(error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
/**
|
|
214
238
|
* Declare what following a kind of thing MEANS: the verb clients render,
|
|
215
239
|
* whether reverse lookups are public, whether it federates.
|