@oxy.so/core 4.6.0 → 4.9.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.
@@ -1,4 +1,65 @@
1
1
  declare const dict: {
2
+ organizationDirectory: {
3
+ invalidName: string;
4
+ review: {
5
+ title: string;
6
+ forbidden: string;
7
+ empty: string;
8
+ hint: string;
9
+ independent: string;
10
+ evidence: string;
11
+ approved: string;
12
+ rejected: string;
13
+ conflict: string;
14
+ failed: string;
15
+ };
16
+ title: string;
17
+ search: string;
18
+ searchHint: string;
19
+ empty: string;
20
+ retry: string;
21
+ loadMore: string;
22
+ failed: string;
23
+ unclaimed: string;
24
+ claimed: string;
25
+ inactive: string;
26
+ newAccount: string;
27
+ newAccountHint: string;
28
+ addDirectory: string;
29
+ directoryHint: string;
30
+ added: string;
31
+ back: string;
32
+ type: string;
33
+ types: {
34
+ business: string;
35
+ nonprofit: string;
36
+ public_body: string;
37
+ community: string;
38
+ other: string;
39
+ };
40
+ name: string;
41
+ legalName: string;
42
+ website: string;
43
+ country: string;
44
+ save: string;
45
+ saved: string;
46
+ invalid: string;
47
+ saveFailed: string;
48
+ claim: string;
49
+ claimHint: string;
50
+ evidence: string;
51
+ evidenceHint: string;
52
+ claimSent: string;
53
+ claimFailed: string;
54
+ claimedHint: string;
55
+ requests: string;
56
+ pending: string;
57
+ approved: string;
58
+ rejected: string;
59
+ requestError: string;
60
+ view: string;
61
+ readonly: string;
62
+ };
2
63
  signin: {
3
64
  title: string;
4
65
  subtitle: string;
@@ -1364,6 +1425,13 @@ declare const dict: {
1364
1425
  };
1365
1426
  };
1366
1427
  preferences: {
1428
+ displayCurrency: {
1429
+ label: string;
1430
+ saveFailed: string;
1431
+ unavailable: string;
1432
+ approximate: string;
1433
+ estimate: string;
1434
+ };
1367
1435
  title: string;
1368
1436
  subtitle: string;
1369
1437
  sections: {
@@ -99,3 +99,6 @@ export { AUTH_WEB_ORIGIN, CENTRAL_IDP_APEX } from './utils/authWebUrl';
99
99
  export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, OXY_OAUTH_STATE_STORAGE_KEY, OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, OXY_OAUTH_REDIRECT_URI_STORAGE_KEY, OXY_OAUTH_RETURN_PATH_STORAGE_KEY, normalizeOAuthRedirectUri, canonicalizeOAuthRedirectUri, persistOAuthHandshake, readOAuthHandshake, clearOAuthHandshake, persistOAuthReturnPath, consumeOAuthReturnPath, } from './utils/oauthPkce';
100
100
  export type { PkcePair, BuildOAuthAuthorizeUrlParams, OxyAuthScreen } from './utils/oauthPkce';
101
101
  export { packageInfo } from './constants/version';
102
+ export type { Organization, OrganizationType, CreateOrganization, UpdateOrganization, OrganizationSearch, OrganizationClaim, } from '@oxy.so/contracts';
103
+ export type { ProfileAccessInput, ProfileAccessDecision } from '@oxy.so/contracts';
104
+ export type { AccountInvitation, AccountInvitationPreview } from './api/accounts';
@@ -242,6 +242,8 @@ export interface NotificationPreferences {
242
242
  * Persisted on the User document.
243
243
  */
244
244
  export interface UserPreferences {
245
+ /** Preferred display units only; never changes a payment's denomination. */
246
+ displayCurrency?: 'USD' | 'EUR' | 'FAIR';
245
247
  /** BCP-47 language tag, e.g. "en-US", "es-ES". Empty string = follow device. */
246
248
  language?: string;
247
249
  /** Theme mode preference. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/core",
3
- "version": "4.6.0",
3
+ "version": "4.9.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -176,11 +176,11 @@
176
176
  }
177
177
  },
178
178
  "dependencies": {
179
- "@oxy.so/telemetry": "^1.2.0",
180
179
  "@noble/ciphers": "^1.3.0",
181
180
  "@noble/hashes": "^1.8.0",
182
- "@oxy.so/contracts": "4.13.0",
181
+ "@oxy.so/contracts": "4.15.0",
183
182
  "@oxy.so/protocol": "^1.2.2",
183
+ "@oxy.so/telemetry": "^1.2.0",
184
184
  "@scure/bip39": "^1.6.0",
185
185
  "buffer": "^6.0.3",
186
186
  "express-rate-limit": "^8.6.0",
@@ -1,3 +1,4 @@
1
+ import { SafetyApi } from './api/safety';
1
2
  /**
2
3
  * `OxyServices` — the Oxy API client.
3
4
  *
@@ -32,6 +33,7 @@ import { PrivacyApi } from './api/privacy';
32
33
  import { NotificationsApi } from './api/notifications';
33
34
  import { AssetsApi } from './api/assets';
34
35
  import { IdentityApi } from './api/identity';
36
+ import { OrganizationsApi } from './api/organizations';
35
37
  import { AccountsApi } from './api/accounts';
36
38
  import { AppsApi } from './api/apps';
37
39
  import { LinkedAccountsApi } from './api/linkedAccounts';
@@ -191,6 +193,8 @@ export class OxyServices {
191
193
  private _assets?: AssetsApi;
192
194
  private _identity?: IdentityApi;
193
195
  private _accounts?: AccountsApi;
196
+ private _organizations?: OrganizationsApi;
197
+ private _safety?: SafetyApi;
194
198
  private _apps?: AppsApi;
195
199
  private _linkedAccounts?: LinkedAccountsApi;
196
200
  private _agency?: AgencyApi;
@@ -245,6 +249,16 @@ export class OxyServices {
245
249
  return this._identity;
246
250
  }
247
251
  /** The account graph: accounts and their members. */
252
+ get safety(): SafetyApi {
253
+ if (!this._safety) this._safety = new SafetyApi(this.context);
254
+ return this._safety;
255
+ }
256
+
257
+ get organizations(): OrganizationsApi {
258
+ if (!this._organizations) this._organizations = new OrganizationsApi(this.context);
259
+ return this._organizations;
260
+ }
261
+
248
262
  get accounts(): AccountsApi {
249
263
  if (!this._accounts) this._accounts = new AccountsApi(this.context);
250
264
  return this._accounts;
@@ -1,3 +1,4 @@
1
+ import type { OrganizationType } from '@oxy.so/contracts';
1
2
  /**
2
3
  * `oxy.accounts` — the unified account graph (`/accounts`).
3
4
  *
@@ -236,6 +237,7 @@ export interface CreateAccountInput {
236
237
  * keep working. At most `MAX_ACCOUNT_CATEGORIES`, no duplicates.
237
238
  */
238
239
  accountCategories?: AccountCategoryId[];
240
+ organizationType?: OrganizationType;
239
241
  /**
240
242
  * Create the account already opted OUT of discovery — kept out of people
241
243
  * search, the follow-graph lists, `/similar` and the recommendation pools,
@@ -301,6 +303,7 @@ export interface UpdateAccountInput {
301
303
  * every other field here (`account:update`).
302
304
  */
303
305
  character?: AvatarCharacter;
306
+ organizationType?: OrganizationType;
304
307
  }
305
308
 
306
309
  /** Input accepted by `accounts.members.invite`. The owner role cannot be invited. */
@@ -388,7 +391,8 @@ export class AccountMembersApi {
388
391
  }
389
392
 
390
393
  /**
391
- * Add a member to an account.
394
+ * @deprecated The API returns 410. Use accounts.invitations.create with an email.
395
+ * Access is granted only after recipient confirmation.
392
396
  * @param accountId - The account's `_id`.
393
397
  * @param data - Target user's username or email and role (never `owner`).
394
398
  * The server resolves `usernameOrEmail` to a personal account; an unknown
@@ -473,13 +477,93 @@ export class AccountAgentKeysApi {
473
477
  }
474
478
  }
475
479
 
480
+ export interface AccountInvitation {
481
+ id: string;
482
+ accountId: string;
483
+ email: string;
484
+ role: Exclude<AccountRole, 'owner'>;
485
+ status: 'pending' | 'expired' | 'accepted' | 'cancelled';
486
+ expiresAt: string;
487
+ createdAt: string;
488
+ }
489
+ export interface AccountInvitationPreview {
490
+ invitation: AccountInvitation;
491
+ account: { id: string; username: string | null; displayName: string | null };
492
+ }
493
+
494
+ /** Consent-based invitations; never cached or queued as offline mutations. */
495
+ export class AccountInvitationsApi {
496
+ constructor(private readonly ctx: OxyContext) {}
497
+ async list(accountId: string): Promise<AccountInvitation[]> {
498
+ return (
499
+ await this.ctx.request<{ invitations: AccountInvitation[] }>(
500
+ 'GET',
501
+ `/accounts/${enc(accountId)}/invitations`,
502
+ undefined,
503
+ { cache: false },
504
+ )
505
+ ).invitations;
506
+ }
507
+ async create(
508
+ accountId: string,
509
+ data: { email: string; role: Exclude<AccountRole, 'owner'>; inherit?: boolean },
510
+ ): Promise<AccountInvitation> {
511
+ return (
512
+ await this.ctx.request<{ invitation: AccountInvitation }>(
513
+ 'POST',
514
+ `/accounts/${enc(accountId)}/invitations`,
515
+ data,
516
+ { cache: false },
517
+ )
518
+ ).invitation;
519
+ }
520
+ async resend(accountId: string, id: string): Promise<AccountInvitation> {
521
+ return (
522
+ await this.ctx.request<{ invitation: AccountInvitation }>(
523
+ 'POST',
524
+ `/accounts/${enc(accountId)}/invitations/${enc(id)}/resend`,
525
+ undefined,
526
+ { cache: false },
527
+ )
528
+ ).invitation;
529
+ }
530
+ async cancel(accountId: string, id: string): Promise<AccountSuccessResult> {
531
+ return this.ctx.request(
532
+ 'DELETE',
533
+ `/accounts/${enc(accountId)}/invitations/${enc(id)}`,
534
+ undefined,
535
+ { cache: false },
536
+ );
537
+ }
538
+ async preview(token: string): Promise<AccountInvitationPreview> {
539
+ return this.ctx.request('POST', '/accounts/invitations/preview', { token }, { cache: false });
540
+ }
541
+ /** selectedUserId must equal the currently authenticated personal account. */
542
+ async accept(
543
+ token: string,
544
+ selectedUserId: string,
545
+ ): Promise<AccountSuccessResult & { accountId: string }> {
546
+ const result = await this.ctx.request<AccountSuccessResult & { accountId: string }>(
547
+ 'POST',
548
+ '/accounts/invitations/accept',
549
+ { token, selectedUserId, confirmed: true },
550
+ { cache: false },
551
+ );
552
+ invalidateAccountMembership(this.ctx, result.accountId);
553
+ evictOxyAccountForestCache(this.ctx.http);
554
+ return result;
555
+ }
556
+ }
557
+
476
558
  export class AccountsApi {
477
559
  /** Who can act on an account, and with what role. */
478
560
  readonly members: AccountMembersApi;
561
+ readonly invitations: AccountInvitationsApi;
479
562
  readonly agentKeys: AccountAgentKeysApi;
480
563
 
481
564
  constructor(private readonly ctx: OxyContext) {
482
565
  this.members = new AccountMembersApi(ctx);
566
+ this.invitations = new AccountInvitationsApi(ctx);
483
567
  this.agentKeys = new AccountAgentKeysApi(ctx);
484
568
  }
485
569
 
@@ -1,5 +1,11 @@
1
1
  import {
2
2
  personalPlanCheckoutRequestSchema,
3
+ productPlanCheckoutRequestSchema,
4
+ productSubscriptionRenewalRequestSchema,
5
+ type ProductSubscriptionRenewalRequest,
6
+ productPlanCatalogueSchema,
7
+ type ProductPlanCheckoutRequest,
8
+ type ProductPlanCatalogue,
3
9
  personalPlanCheckoutResultSchema,
4
10
  type PersonalPlanCheckoutRequest,
5
11
  type PersonalPlanCheckoutResult,
@@ -127,6 +133,43 @@ export interface WalletTransactionsPage {
127
133
  export class BillingApi {
128
134
  constructor(protected readonly ctx: OxyContext) {}
129
135
 
136
+ async renewProductSubscription(
137
+ request: ProductSubscriptionRenewalRequest,
138
+ ): Promise<PersonalPlanCheckoutResult> {
139
+ return personalPlanCheckoutResultSchema.parse(
140
+ await this.ctx.request<unknown>(
141
+ 'POST',
142
+ '/billing/product-subscriptions/renew',
143
+ productSubscriptionRenewalRequestSchema.parse(request),
144
+ { cache: false },
145
+ ),
146
+ );
147
+ }
148
+
149
+ async startProductPlanCheckout(
150
+ request: ProductPlanCheckoutRequest,
151
+ ): Promise<PersonalPlanCheckoutResult> {
152
+ return personalPlanCheckoutResultSchema.parse(
153
+ await this.ctx.request<unknown>(
154
+ 'POST',
155
+ '/billing/checkout/product-plan',
156
+ productPlanCheckoutRequestSchema.parse(request),
157
+ { cache: false },
158
+ ),
159
+ );
160
+ }
161
+
162
+ async productPlans(productId: string): Promise<ProductPlanCatalogue> {
163
+ return productPlanCatalogueSchema.parse(
164
+ await this.ctx.request<unknown>(
165
+ 'GET',
166
+ `/billing/product-plans/${encodeURIComponent(productId)}`,
167
+ undefined,
168
+ { cache: false },
169
+ ),
170
+ );
171
+ }
172
+
130
173
  async startPersonalPlanCheckout(
131
174
  request: PersonalPlanCheckoutRequest,
132
175
  ): Promise<PersonalPlanCheckoutResult> {
@@ -0,0 +1,180 @@
1
+ import {
2
+ organizationSchema,
3
+ organizationListSchema,
4
+ organizationBatchSchema,
5
+ organizationClaimSchema,
6
+ organizationExternalSchema,
7
+ organizationExternalResolutionSchema,
8
+ organizationImportBatchResultSchema,
9
+ organizationCurationResultSchema,
10
+ type OrganizationCuration,
11
+ type OrganizationImportBatch,
12
+ organizationClaimListSchema,
13
+ organizationClaimDetailSchema,
14
+ type OrganizationClaimListQuery,
15
+ type CreateOrganization,
16
+ type OrganizationSearch,
17
+ type UpdateOrganization,
18
+ type OrganizationClaimInput,
19
+ type OrganizationClaimReview,
20
+ type OrganizationExternalInput,
21
+ type OrganizationMerge,
22
+ } from '@oxy.so/contracts';
23
+ import type { OxyContext } from '../client/context';
24
+ import { evictOxyIdentityCache } from '../utils/identityCacheSweep';
25
+
26
+ /** The public canonical organization directory. Claiming changes authority, never identity. */
27
+ export class OrganizationsApi {
28
+ constructor(private readonly ctx: OxyContext) {}
29
+ async search(input: OrganizationSearch) {
30
+ return organizationListSchema.parse(
31
+ await this.ctx.request('GET', '/organizations', input, { cache: false, skipAuth: true }),
32
+ );
33
+ }
34
+ async get(accountId: string) {
35
+ return organizationSchema.parse(
36
+ await this.ctx.request('GET', `/organizations/${encodeURIComponent(accountId)}`, undefined, {
37
+ cache: false,
38
+ skipAuth: true,
39
+ }),
40
+ );
41
+ }
42
+ async batch(input: { ids: string[] }) {
43
+ return organizationBatchSchema.parse(
44
+ await this.ctx.request('POST', '/organizations/batch', input, {
45
+ cache: false,
46
+ skipAuth: true,
47
+ }),
48
+ );
49
+ }
50
+ async create(input: CreateOrganization) {
51
+ return organizationSchema.parse(
52
+ await this.ctx.request('POST', '/organizations', input, { cache: false }),
53
+ );
54
+ }
55
+ async update(accountId: string, input: UpdateOrganization) {
56
+ const result = organizationSchema.parse(
57
+ await this.ctx.request('PATCH', `/organizations/${encodeURIComponent(accountId)}`, input, {
58
+ cache: false,
59
+ }),
60
+ );
61
+
62
+ evictOxyIdentityCache(this.ctx.http, accountId);
63
+ return result;
64
+ }
65
+ async claim(accountId: string, input: OrganizationClaimInput) {
66
+ return organizationClaimSchema.parse(
67
+ await this.ctx.request(
68
+ 'POST',
69
+ `/organizations/${encodeURIComponent(accountId)}/claims`,
70
+ input,
71
+ { cache: false },
72
+ ),
73
+ );
74
+ }
75
+ async myClaims(input: OrganizationClaimListQuery = {}) {
76
+ return organizationClaimListSchema.parse(
77
+ await this.ctx.request('GET', '/organizations/claims/mine', input, { cache: false }),
78
+ );
79
+ }
80
+ async myClaim(claimId: string) {
81
+ return organizationClaimDetailSchema.parse(
82
+ await this.ctx.request(
83
+ 'GET',
84
+ `/organizations/claims/mine/${encodeURIComponent(claimId)}`,
85
+ undefined,
86
+ { cache: false },
87
+ ),
88
+ );
89
+ }
90
+ async reviewClaims(input: OrganizationClaimListQuery = {}) {
91
+ return organizationClaimListSchema.parse(
92
+ await this.ctx.request('GET', '/organizations/claims/review', input, { cache: false }),
93
+ );
94
+ }
95
+ async reviewClaimDetail(claimId: string) {
96
+ return organizationClaimDetailSchema.parse(
97
+ await this.ctx.request(
98
+ 'GET',
99
+ `/organizations/claims/review/${encodeURIComponent(claimId)}`,
100
+ undefined,
101
+ { cache: false },
102
+ ),
103
+ );
104
+ }
105
+ async reviewClaim(claimId: string, input: OrganizationClaimReview) {
106
+ const result = organizationClaimSchema.parse(
107
+ await this.ctx.request(
108
+ 'POST',
109
+ `/organizations/claims/${encodeURIComponent(claimId)}/review`,
110
+ input,
111
+ { cache: false },
112
+ ),
113
+ );
114
+
115
+ evictOxyIdentityCache(this.ctx.http, result.accountId);
116
+ return result;
117
+ }
118
+ async importBatch(input: OrganizationImportBatch) {
119
+ if (!this.ctx.service?.available)
120
+ throw new Error('Organization batch import requires an OxyServer service identity');
121
+ return organizationImportBatchResultSchema.parse(
122
+ await this.ctx.service.request('POST', '/organizations/external/batch', input, {
123
+ cache: false,
124
+ }),
125
+ );
126
+ }
127
+ async importExternal(input: OrganizationExternalInput) {
128
+ return organizationExternalSchema.parse(
129
+ await this.ctx.request('POST', '/organizations/external', input, { cache: false }),
130
+ );
131
+ }
132
+ async resolveExternal(input: { namespace: string; externalId: string }) {
133
+ return organizationExternalResolutionSchema.parse(
134
+ await this.ctx.request(
135
+ 'GET',
136
+ `/organizations/external/${encodeURIComponent(input.namespace)}/${encodeURIComponent(input.externalId)}`,
137
+ undefined,
138
+ { cache: false },
139
+ ),
140
+ );
141
+ }
142
+ async curate(accountId: string, input: OrganizationCuration) {
143
+ const result = organizationCurationResultSchema.parse(
144
+ await this.ctx.request(
145
+ 'POST',
146
+ `/organizations/${encodeURIComponent(accountId)}/curation`,
147
+ input,
148
+ { cache: false },
149
+ ),
150
+ );
151
+
152
+ evictOxyIdentityCache(this.ctx.http, accountId);
153
+ return result;
154
+ }
155
+ async merge(accountId: string, input: OrganizationMerge) {
156
+ const result = organizationSchema.parse(
157
+ await this.ctx.request(
158
+ 'POST',
159
+ `/organizations/${encodeURIComponent(accountId)}/merge`,
160
+ input,
161
+ { cache: false },
162
+ ),
163
+ );
164
+
165
+ evictOxyIdentityCache(this.ctx.http, accountId);
166
+ evictOxyIdentityCache(this.ctx.http, result.accountId);
167
+ return result;
168
+ }
169
+ async *iterate(input: OrganizationSearch) {
170
+ let cursor = input.cursor;
171
+ const seen = new Set<string>();
172
+ do {
173
+ const page = await this.search({ ...input, cursor });
174
+ yield* page.items;
175
+ cursor = page.nextCursor ?? undefined;
176
+ if (cursor && seen.has(cursor)) throw new Error('Organization directory repeated a cursor');
177
+ if (cursor) seen.add(cursor);
178
+ } while (cursor);
179
+ }
180
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ profileAccessInputSchema,
3
+ profileAccessDecisionSchema,
4
+ type ProfileAccessInput,
5
+ } from '@oxy.so/contracts';
6
+ import type { OxyContext } from '../client/context';
7
+
8
+ export class SafetyApi {
9
+ constructor(private readonly ctx: OxyContext) {}
10
+ /** Server callers must explicitly supply the incoming user bearer or null for anonymous access. */
11
+ async profileAccess(input: ProfileAccessInput, options?: { accessToken: string | null }) {
12
+ if (this.ctx.service && options === undefined)
13
+ throw new Error('Server profile access requires an explicit requester bearer or null');
14
+ return profileAccessDecisionSchema.parse(
15
+ await this.ctx.request(
16
+ 'GET',
17
+ '/safety/profile-access',
18
+ profileAccessInputSchema.parse(input),
19
+ {
20
+ cache: false,
21
+ ...(options
22
+ ? {
23
+ skipAuth: true,
24
+ headers: options.accessToken
25
+ ? { Authorization: `Bearer ${options.accessToken}` }
26
+ : {},
27
+ }
28
+ : {}),
29
+ },
30
+ ),
31
+ );
32
+ }
33
+ }
@@ -1,4 +1,72 @@
1
1
  const dict = {
2
+ organizationDirectory: {
3
+ invalidName: 'Use a plain-text organization name (up to 300 characters).',
4
+ review: {
5
+ title: 'Review organization access requests',
6
+ forbidden: 'This action requires current staff authorization.',
7
+ empty: 'No requests are waiting for review.',
8
+ hint: 'Review the supporting evidence independently. Approval grants management access to the existing organization account.',
9
+ independent: 'Another reviewer must decide your own request.',
10
+ evidence: 'Review evidence reference',
11
+ approved: 'Approve access',
12
+ rejected: 'Reject request',
13
+ conflict:
14
+ 'This request or organization changed. Refresh the queue before making another decision.',
15
+ failed: 'The review could not be saved. Your decision has not been confirmed.',
16
+ },
17
+ title: 'Organization directory',
18
+ search: 'Find your organization',
19
+ searchHint:
20
+ 'Find an existing organization before creating an account. Its identity is shared across Oxy apps.',
21
+ empty: 'No organizations found.',
22
+ retry: 'Try again',
23
+ loadMore: 'Show more',
24
+ failed: 'Could not load the organization directory.',
25
+ unclaimed: 'Unclaimed',
26
+ claimed: 'Managed organization',
27
+ inactive: 'Inactive',
28
+ newAccount: 'Create a new organization account',
29
+ newAccountHint:
30
+ 'Create and manage a new organization. Choose an existing entry if your organization is already listed.',
31
+ addDirectory: 'Add to directory',
32
+ directoryHint:
33
+ 'This creates a shared, unclaimed entry. It does not give you access to manage the organization.',
34
+ added: 'Organization added to the directory',
35
+ back: 'Back to search',
36
+ type: 'Organization type',
37
+ types: {
38
+ business: 'Business',
39
+ nonprofit: 'Nonprofit',
40
+ public_body: 'Public body',
41
+ community: 'Community',
42
+ other: 'Other',
43
+ },
44
+ name: 'Organization name',
45
+ legalName: 'Legal name',
46
+ website: 'Website',
47
+ country: 'Country code',
48
+ save: 'Save organization',
49
+ saved: 'Organization updated',
50
+ invalid:
51
+ 'Check the organization details. Use a complete website URL and a two-letter country code.',
52
+ saveFailed: 'Could not save the organization. Try again.',
53
+ claim: 'Request management access',
54
+ claimHint:
55
+ 'Oxy reviews evidence of your authority. A request does not grant access or verify your role.',
56
+ evidence: 'Supporting evidence reference',
57
+ evidenceHint:
58
+ 'Provide a reference to your private verification case or supporting records. Only you and reviewers can read it.',
59
+ claimSent: 'Request submitted for review',
60
+ claimFailed: 'Could not submit your request. Try again.',
61
+ claimedHint: 'This organization already has managers. Contact a manager to request access.',
62
+ requests: 'Organization access requests',
63
+ pending: 'Pending review',
64
+ approved: 'Approved',
65
+ rejected: 'Rejected',
66
+ requestError: 'Could not load your access requests.',
67
+ view: 'Organization details',
68
+ readonly: 'You do not have permission to edit this organization.',
69
+ },
2
70
  signin: {
3
71
  title: 'Sign in',
4
72
  subtitle: 'Use your Oxy account',
@@ -1397,6 +1465,14 @@ const dict = {
1397
1465
  },
1398
1466
  },
1399
1467
  preferences: {
1468
+ displayCurrency: {
1469
+ label: 'Display currency',
1470
+ saveFailed: 'Could not save display currency. Please try again.',
1471
+ unavailable: 'Equivalent in {{currency}} unavailable',
1472
+ approximate: 'Approx. {{amount}}',
1473
+ estimate: 'Display estimate. The final payable amount is quoted in FAIR at checkout.',
1474
+ },
1475
+
1400
1476
  title: 'Preferences',
1401
1477
  subtitle: 'Theme, motion, and regional settings',
1402
1478
  sections: {