@oxy.so/core 1.16.0 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +30 -1
  3. package/dist/cjs/crypto/identityLink.js +18 -0
  4. package/dist/cjs/i18n/locales/ar-SA.json +78 -3
  5. package/dist/cjs/i18n/locales/ca-ES.json +78 -3
  6. package/dist/cjs/i18n/locales/de-DE.json +78 -3
  7. package/dist/cjs/i18n/locales/en-US.json +75 -28
  8. package/dist/cjs/i18n/locales/es-ES.json +75 -28
  9. package/dist/cjs/i18n/locales/fr-FR.json +78 -3
  10. package/dist/cjs/i18n/locales/it-IT.json +78 -3
  11. package/dist/cjs/i18n/locales/ja-JP.json +78 -3
  12. package/dist/cjs/i18n/locales/ko-KR.json +78 -3
  13. package/dist/cjs/i18n/locales/locales/ar-SA.json +78 -3
  14. package/dist/cjs/i18n/locales/locales/ca-ES.json +78 -3
  15. package/dist/cjs/i18n/locales/locales/de-DE.json +78 -3
  16. package/dist/cjs/i18n/locales/locales/en-US.json +75 -28
  17. package/dist/cjs/i18n/locales/locales/es-ES.json +75 -28
  18. package/dist/cjs/i18n/locales/locales/fr-FR.json +78 -3
  19. package/dist/cjs/i18n/locales/locales/it-IT.json +78 -3
  20. package/dist/cjs/i18n/locales/locales/ja-JP.json +78 -3
  21. package/dist/cjs/i18n/locales/locales/ko-KR.json +78 -3
  22. package/dist/cjs/i18n/locales/locales/pt-PT.json +78 -3
  23. package/dist/cjs/i18n/locales/locales/zh-CN.json +78 -3
  24. package/dist/cjs/i18n/locales/pt-PT.json +78 -3
  25. package/dist/cjs/i18n/locales/zh-CN.json +78 -3
  26. package/dist/cjs/index.js +9 -42
  27. package/dist/cjs/mixins/OxyServices.auth.js +110 -17
  28. package/dist/cjs/mixins/OxyServices.identity.js +128 -8
  29. package/dist/cjs/mixins/OxyServices.user.js +42 -7
  30. package/dist/cjs/utils/authWebUrl.js +4 -5
  31. package/dist/esm/.tsbuildinfo +1 -1
  32. package/dist/esm/HttpService.js +30 -1
  33. package/dist/esm/crypto/identityLink.js +15 -0
  34. package/dist/esm/i18n/locales/ar-SA.json +78 -3
  35. package/dist/esm/i18n/locales/ca-ES.json +78 -3
  36. package/dist/esm/i18n/locales/de-DE.json +78 -3
  37. package/dist/esm/i18n/locales/en-US.json +75 -28
  38. package/dist/esm/i18n/locales/es-ES.json +75 -28
  39. package/dist/esm/i18n/locales/fr-FR.json +78 -3
  40. package/dist/esm/i18n/locales/it-IT.json +78 -3
  41. package/dist/esm/i18n/locales/ja-JP.json +78 -3
  42. package/dist/esm/i18n/locales/ko-KR.json +78 -3
  43. package/dist/esm/i18n/locales/locales/ar-SA.json +78 -3
  44. package/dist/esm/i18n/locales/locales/ca-ES.json +78 -3
  45. package/dist/esm/i18n/locales/locales/de-DE.json +78 -3
  46. package/dist/esm/i18n/locales/locales/en-US.json +75 -28
  47. package/dist/esm/i18n/locales/locales/es-ES.json +75 -28
  48. package/dist/esm/i18n/locales/locales/fr-FR.json +78 -3
  49. package/dist/esm/i18n/locales/locales/it-IT.json +78 -3
  50. package/dist/esm/i18n/locales/locales/ja-JP.json +78 -3
  51. package/dist/esm/i18n/locales/locales/ko-KR.json +78 -3
  52. package/dist/esm/i18n/locales/locales/pt-PT.json +78 -3
  53. package/dist/esm/i18n/locales/locales/zh-CN.json +78 -3
  54. package/dist/esm/i18n/locales/pt-PT.json +78 -3
  55. package/dist/esm/i18n/locales/zh-CN.json +78 -3
  56. package/dist/esm/index.js +4 -4
  57. package/dist/esm/mixins/OxyServices.auth.js +110 -17
  58. package/dist/esm/mixins/OxyServices.identity.js +128 -8
  59. package/dist/esm/mixins/OxyServices.user.js +42 -7
  60. package/dist/esm/utils/authWebUrl.js +4 -5
  61. package/dist/types/.tsbuildinfo +1 -1
  62. package/dist/types/HttpService.d.ts +21 -0
  63. package/dist/types/crypto/identityLink.d.ts +8 -0
  64. package/dist/types/index.d.ts +2 -5
  65. package/dist/types/mixins/OxyServices.auth.d.ts +68 -25
  66. package/dist/types/mixins/OxyServices.identity.d.ts +34 -34
  67. package/dist/types/mixins/OxyServices.user.d.ts +29 -6
  68. package/dist/types/models/interfaces.d.ts +67 -13
  69. package/dist/types/server/securityHeaders.d.ts +3 -3
  70. package/dist/types/utils/authWebUrl.d.ts +4 -5
  71. package/package.json +3 -3
  72. package/src/HttpService.ts +35 -1
  73. package/src/__tests__/serviceIdentityWhenAnonymous.test.ts +159 -0
  74. package/src/crypto/__tests__/identityLink.test.ts +18 -0
  75. package/src/crypto/__tests__/identityProof.test.ts +6 -6
  76. package/src/crypto/identityLink.ts +18 -0
  77. package/src/i18n/locales/ar-SA.json +78 -3
  78. package/src/i18n/locales/ca-ES.json +78 -3
  79. package/src/i18n/locales/de-DE.json +78 -3
  80. package/src/i18n/locales/en-US.json +75 -28
  81. package/src/i18n/locales/es-ES.json +75 -28
  82. package/src/i18n/locales/fr-FR.json +78 -3
  83. package/src/i18n/locales/it-IT.json +78 -3
  84. package/src/i18n/locales/ja-JP.json +78 -3
  85. package/src/i18n/locales/ko-KR.json +78 -3
  86. package/src/i18n/locales/pt-PT.json +78 -3
  87. package/src/i18n/locales/zh-CN.json +78 -3
  88. package/src/index.ts +6 -48
  89. package/src/mixins/OxyServices.auth.ts +127 -24
  90. package/src/mixins/OxyServices.identity.ts +126 -8
  91. package/src/mixins/OxyServices.user.ts +51 -10
  92. package/src/mixins/__tests__/identityLink.test.ts +89 -0
  93. package/src/mixins/__tests__/notifications.read.test.ts +82 -0
  94. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +1 -1
  95. package/src/mixins/__tests__/webauthnAuth.test.ts +75 -1
  96. package/src/models/interfaces.ts +67 -12
  97. package/src/server/securityHeaders.ts +3 -3
  98. package/src/utils/authWebUrl.ts +4 -5
  99. package/dist/cjs/crypto/identityMove.js +0 -142
  100. package/dist/cjs/crypto/webIdentityCarrier.js +0 -325
  101. package/dist/esm/crypto/identityMove.js +0 -128
  102. package/dist/esm/crypto/webIdentityCarrier.js +0 -303
  103. package/dist/types/crypto/identityMove.d.ts +0 -75
  104. package/dist/types/crypto/webIdentityCarrier.d.ts +0 -150
  105. package/src/crypto/__tests__/identityMove.test.ts +0 -146
  106. package/src/crypto/__tests__/webIdentityCarrier.test.ts +0 -169
  107. package/src/crypto/__tests__/webIdentityRecoveryMaterial.test.ts +0 -117
  108. package/src/crypto/identityMove.ts +0 -167
  109. package/src/crypto/webIdentityCarrier.ts +0 -394
@@ -0,0 +1,159 @@
1
+ /**
2
+ * `serviceIdentity: 'when-anonymous'` — a backend's session-less reads carry
3
+ * its service token.
4
+ *
5
+ * oxy-api charges anonymous requests to their source address, and a backend
6
+ * fleet leaves through one NAT address, so past 100 anonymous requests in 15
7
+ * minutes EVERY anonymous read that backend makes pays a +500 ms `slowDown`.
8
+ * Mention measured 543–575 ms for `GET /users/:id` anonymous against ~20 ms
9
+ * with its service token (OxyHQ/Mention#1173). A first-party service token is
10
+ * exempt, so a backend should present one on every read that has no user.
11
+ *
12
+ * What is asserted is the `Authorization` header that reaches `fetch`, because
13
+ * that is the only thing oxy-api's limiter sees.
14
+ */
15
+
16
+ import { OxyServices } from '../OxyServices';
17
+ import { ANONYMOUS_SERVICE_TOKEN_RETRY_MS } from '../mixins/OxyServices.auth';
18
+
19
+ interface FetchCall {
20
+ url: string;
21
+ authorization: string | undefined;
22
+ }
23
+
24
+ function jsonResponse(data: unknown): Response {
25
+ return new Response(JSON.stringify({ data }), {
26
+ status: 200,
27
+ headers: { 'content-type': 'application/json' },
28
+ });
29
+ }
30
+
31
+ function authorizationOf(init: RequestInit | undefined): string | undefined {
32
+ const headers = init?.headers;
33
+ if (!headers) return undefined;
34
+ if (headers instanceof Headers) return headers.get('authorization') ?? undefined;
35
+ const record = headers as Record<string, string>;
36
+ return record.Authorization ?? record.authorization;
37
+ }
38
+
39
+ function createJwt(payload: Record<string, unknown>): string {
40
+ const encode = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString('base64url');
41
+ return `${encode({ alg: 'EdDSA', typ: 'JWT' })}.${encode(payload)}.signature`;
42
+ }
43
+
44
+ describe("OxyServices serviceIdentity: 'when-anonymous'", () => {
45
+ const originalFetch = globalThis.fetch;
46
+ let calls: FetchCall[];
47
+
48
+ beforeEach(() => {
49
+ calls = [];
50
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
51
+ calls.push({ url: String(input), authorization: authorizationOf(init) });
52
+ return jsonResponse({ id: 'u1', username: 'alice', name: { displayName: 'Alice' } });
53
+ }) as typeof fetch;
54
+ });
55
+
56
+ afterEach(() => {
57
+ globalThis.fetch = originalFetch;
58
+ jest.restoreAllMocks();
59
+ });
60
+
61
+ function backend(): OxyServices {
62
+ return new OxyServices({ baseURL: 'https://api.oxy.test', serviceIdentity: 'when-anonymous' });
63
+ }
64
+
65
+ it('sends a session-less read with the service token', async () => {
66
+ const oxy = backend();
67
+ jest.spyOn(oxy, '_canUseWorkloadIdentity').mockResolvedValue(true);
68
+ const getServiceToken = jest.spyOn(oxy, 'getServiceToken').mockResolvedValue('svc-token');
69
+
70
+ await oxy.getUserById('u1');
71
+ await oxy.getProfileByUsername('alice');
72
+
73
+ expect(calls.map((call) => call.authorization)).toEqual(['Bearer svc-token', 'Bearer svc-token']);
74
+ expect(getServiceToken).toHaveBeenCalledTimes(2);
75
+ });
76
+
77
+ it('uses a configured key pair without asking about attestation', async () => {
78
+ const oxy = backend();
79
+ oxy.configureServiceAuth('oxy_dk_test', 'secret');
80
+ const canAttest = jest.spyOn(oxy, '_canUseWorkloadIdentity');
81
+ jest.spyOn(oxy, 'getServiceToken').mockResolvedValue('keypair-token');
82
+
83
+ await oxy.getUserById('u1');
84
+
85
+ expect(calls[0].authorization).toBe('Bearer keypair-token');
86
+ expect(canAttest).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('lets a user session win over the service token', async () => {
90
+ const oxy = backend();
91
+ const getServiceToken = jest.spyOn(oxy, 'getServiceToken').mockResolvedValue('svc-token');
92
+ const userToken = createJwt({ userId: 'viewer', exp: Math.floor(Date.now() / 1000) + 3600 });
93
+ oxy.setTokens(userToken);
94
+
95
+ await oxy.getUserById('u1');
96
+
97
+ expect(calls[0].authorization).toBe(`Bearer ${userToken}`);
98
+ expect(getServiceToken).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it('stays anonymous where no service token can exist (a local checkout)', async () => {
102
+ const oxy = backend();
103
+ jest.spyOn(oxy, '_canUseWorkloadIdentity').mockResolvedValue(false);
104
+ const getServiceToken = jest.spyOn(oxy, 'getServiceToken');
105
+
106
+ await oxy.getUserById('u1');
107
+
108
+ expect(calls[0].authorization).toBeUndefined();
109
+ expect(getServiceToken).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('sends the read anonymous when the mint fails, and does not retry the mint for a window', async () => {
113
+ const oxy = backend();
114
+ jest.spyOn(oxy, '_canUseWorkloadIdentity').mockResolvedValue(true);
115
+ const getServiceToken = jest
116
+ .spyOn(oxy, 'getServiceToken')
117
+ .mockRejectedValue(new Error('Oxy refused the workload attestation (401)'));
118
+ const now = jest.spyOn(Date, 'now');
119
+ const start = 1_800_000_000_000;
120
+ now.mockReturnValue(start);
121
+
122
+ // Different ids: each is a real request, not a cache hit or a deduplicated one.
123
+ await expect(oxy.getUserById('u1')).resolves.toBeDefined();
124
+ await expect(oxy.getUserById('u2')).resolves.toBeDefined();
125
+
126
+ expect(calls.map((call) => call.authorization)).toEqual([undefined, undefined]);
127
+ expect(getServiceToken).toHaveBeenCalledTimes(1);
128
+
129
+ // After the window the mint is tried again.
130
+ getServiceToken.mockResolvedValue('svc-token');
131
+ now.mockReturnValue(start + ANONYMOUS_SERVICE_TOKEN_RETRY_MS + 1);
132
+ await oxy.getUserById('u3');
133
+
134
+ expect(calls[2].authorization).toBe('Bearer svc-token');
135
+ expect(getServiceToken).toHaveBeenCalledTimes(2);
136
+ });
137
+
138
+ it('never attaches a bearer to a skipAuth request', async () => {
139
+ const oxy = backend();
140
+ jest.spyOn(oxy, '_canUseWorkloadIdentity').mockResolvedValue(true);
141
+ const getServiceToken = jest.spyOn(oxy, 'getServiceToken').mockResolvedValue('svc-token');
142
+
143
+ await oxy.makeRequest('GET', '/auth/lookup/alice', undefined, { skipAuth: true, cache: false });
144
+
145
+ expect(calls[0].authorization).toBeUndefined();
146
+ expect(getServiceToken).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it('is off by default: a client without the option stays anonymous', async () => {
150
+ const oxy = new OxyServices({ baseURL: 'https://api.oxy.test' });
151
+ jest.spyOn(oxy, '_canUseWorkloadIdentity').mockResolvedValue(true);
152
+ const getServiceToken = jest.spyOn(oxy, 'getServiceToken').mockResolvedValue('svc-token');
153
+
154
+ await oxy.getUserById('u1');
155
+
156
+ expect(calls[0].authorization).toBeUndefined();
157
+ expect(getServiceToken).not.toHaveBeenCalled();
158
+ });
159
+ });
@@ -0,0 +1,18 @@
1
+ import { deriveIdentityLinkCode } from '../identityLink';
2
+
3
+ const LINK_ID = 'ab'.repeat(16);
4
+ const KEY = `04${'1f'.repeat(64)}`;
5
+
6
+ describe('deriveIdentityLinkCode', () => {
7
+ it('is 6 digits, the same on both devices whatever the key spelling', () => {
8
+ const code = deriveIdentityLinkCode(LINK_ID, KEY);
9
+ expect(code).toMatch(/^\d{6}$/);
10
+ expect(deriveIdentityLinkCode(LINK_ID, ` ${KEY.toUpperCase()} `)).toBe(code);
11
+ });
12
+
13
+ it('changes with the key and with the request', () => {
14
+ const code = deriveIdentityLinkCode(LINK_ID, KEY);
15
+ expect(deriveIdentityLinkCode(LINK_ID, `04${'2e'.repeat(64)}`)).not.toBe(code);
16
+ expect(deriveIdentityLinkCode('cd'.repeat(16), KEY)).not.toBe(code);
17
+ });
18
+ });
@@ -6,12 +6,13 @@
6
6
  import { buildIdentityProofMessage, canonicalJson, type IdentityProofClaims } from '@oxy.so/contracts';
7
7
  import { verifySignature } from '@oxy.so/protocol';
8
8
  import { digestIdentityPayload, signIdentityProof } from '../identityProof';
9
- import { deriveIdentityFromMnemonic } from '../webIdentityCarrier';
9
+ import { deriveSecp256k1PublicKey } from '@oxy.so/protocol/secp256k1';
10
10
 
11
- const identity = deriveIdentityFromMnemonic('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about');
11
+ const keyPair = (privateKey: string) => ({ privateKey, publicKey: deriveSecp256k1PublicKey(privateKey) });
12
+ const identity = keyPair('11'.repeat(32));
12
13
 
13
14
  const claims = (overrides: Partial<IdentityProofClaims> = {}): IdentityProofClaims => ({
14
- action: 'web_envelope_put',
15
+ action: 'link_identity',
15
16
  subject: 'user-1',
16
17
  actor: 'user-1',
17
18
  rootPublicKey: identity.publicKey,
@@ -42,7 +43,7 @@ describe('canonical JSON', () => {
42
43
  describe('the signed bytes', () => {
43
44
  it('are a fixed, versioned, domain-separated shape', () => {
44
45
  expect(buildIdentityProofMessage(claims())).toBe(
45
- `{"action":"web_envelope_put","actor":"user-1","audience":"oxy-api/identity","challenge":"${'ab'.repeat(32)}","domain":"oxy-identity-proof","expectedRevision":3,"expiresAt":1800000000000,"payloadDigest":"${claims().payloadDigest}","rootPublicKey":"${identity.publicKey}","subject":"user-1","v":2}`,
46
+ `{"action":"link_identity","actor":"user-1","audience":"oxy-api/identity","challenge":"${'ab'.repeat(32)}","domain":"oxy-identity-proof","expectedRevision":3,"expiresAt":1800000000000,"payloadDigest":"${claims().payloadDigest}","rootPublicKey":"${identity.publicKey}","subject":"user-1","v":2}`,
46
47
  );
47
48
  });
48
49
 
@@ -61,7 +62,6 @@ describe('signing', () => {
61
62
 
62
63
  expect(await verifySignature(buildIdentityProofMessage(claims()), proof.signature, identity.publicKey)).toBe(true);
63
64
  for (const changed of [
64
- claims({ action: 'web_envelope_delete' }),
65
65
  claims({ subject: 'user-2' }),
66
66
  claims({ actor: 'user-2' }),
67
67
  claims({ payloadDigest: digestIdentityPayload({ b: 2 }) }),
@@ -75,7 +75,7 @@ describe('signing', () => {
75
75
  });
76
76
 
77
77
  it('will not sign claims naming a different root', async () => {
78
- const other = deriveIdentityFromMnemonic('legal winner thank year wave sausage worth useful legal winner thank yellow');
78
+ const other = keyPair('22'.repeat(32));
79
79
  await expect(signIdentityProof(identity, claims({ rootPublicKey: other.publicKey }))).rejects.toThrow('different root');
80
80
  });
81
81
  });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The code both screens show while Commons is being linked to a passkey
3
+ * account (ADR 0029 D3): 6 digits derived from the link request and the key
4
+ * Commons signed with. auth.oxy.so derives it from the key the API relayed,
5
+ * Commons from its own; the person checks they match before the passkey is
6
+ * asserted, so a photographed QR cannot slip another key in.
7
+ */
8
+
9
+ import { sha256 } from '@noble/hashes/sha256';
10
+ import { utf8ToBytes } from '@noble/hashes/utils';
11
+
12
+ const DOMAIN = 'oxy-identity-link-code/v1';
13
+
14
+ export function deriveIdentityLinkCode(linkId: string, publicKey: string): string {
15
+ const digest = sha256(utf8ToBytes(`${DOMAIN}|${linkId}|${publicKey.trim().toLowerCase()}`));
16
+ const value = ((digest[0] << 24) | (digest[1] << 16) | (digest[2] << 8) | digest[3]) >>> 0;
17
+ return String(value % 1_000_000).padStart(6, '0');
18
+ }
@@ -50,7 +50,26 @@
50
50
  "createInCommons": "أنشئ هويتك في Commons",
51
51
  "createAccount": "إنشاء حساب",
52
52
  "backToSignInLink": "لديك حساب بالفعل؟ سجّل الدخول",
53
- "webSubtitle": "بلا كلمة مرور. ينشئ جهازك مفتاح مرور، ويحصل حسابك على عبارة استرداد تحتفظ بها أنت وحدك."
53
+ "webSubtitle": "بلا كلمة مرور: ينشئ جهازك مفتاح مرور، ويعيدك بريد الاسترداد إذا فقدته.",
54
+ "username": {
55
+ "label": "اسم المستخدم",
56
+ "placeholder": "yourname",
57
+ "required": "اختر اسم مستخدم.",
58
+ "taken": "اسم المستخدم هذا مستخدم بالفعل."
59
+ },
60
+ "email": {
61
+ "title": "بريد الاسترداد",
62
+ "subtitle": "إذا فقدت مفتاح المرور، نرسل إلى هنا رمزًا لتعود. لا يُستخدم لأي شيء آخر.",
63
+ "label": "البريد الإلكتروني",
64
+ "placeholder": "you@example.com",
65
+ "invalid": "أدخل بريدًا إلكترونيًا صالحًا."
66
+ },
67
+ "passkey": {
68
+ "title": "أنشئ مفتاح المرور",
69
+ "subtitle": "تسجّل الدخول به: ببصمتك أو وجهك أو قفل الشاشة. لا شيء تتذكره.",
70
+ "action": "إنشاء مفتاح مرور"
71
+ },
72
+ "commonsInstead": "تريد الاحتفاظ بمفتاحك بنفسك؟ استخدم Commons"
54
73
  },
55
74
  "accountSwitcher": {
56
75
  "loading": "جارٍ تحميل الحسابات...",
@@ -225,7 +244,63 @@
225
244
  "help": "المساعدة والملاحظات",
226
245
  "signOut": "تسجيل الخروج",
227
246
  "privacy": "سياسة الخصوصية",
228
- "terms": "شروط الخدمة",
229
- "identity": "هويتك"
247
+ "terms": "شروط الخدمة"
248
+ },
249
+ "recover": {
250
+ "title": "استرد حسابك",
251
+ "subtitle": "أدخل اسم المستخدم أو بريد الاسترداد. سنرسل رمزًا إلى بريد الاسترداد الخاص بالحساب.",
252
+ "identifier": {
253
+ "label": "اسم المستخدم أو البريد",
254
+ "placeholder": "yourname",
255
+ "required": "أدخل اسم المستخدم أو البريد."
256
+ },
257
+ "commonsNote": "هل يستخدم حسابك Commons؟ استرده في Commons بعبارة الاسترداد.",
258
+ "passkey": {
259
+ "title": "أضف مفتاح مرور جديدًا",
260
+ "subtitle": "لـ @{{username}}. أزل مفاتيح المرور المفقودة لاحقًا من إعدادات أمان حسابك.",
261
+ "action": "إنشاء مفتاح مرور"
262
+ },
263
+ "backToSignIn": "العودة إلى تسجيل الدخول"
264
+ },
265
+ "emailCode": {
266
+ "title": "تحقق من بريدك",
267
+ "sentTo": "أرسلنا رمزًا من 6 أرقام إلى {{email}}.",
268
+ "sentIfAccount": "إذا كان لهذا الحساب بريد استرداد، فقد أرسلنا إليه رمزًا من 6 أرقام.",
269
+ "label": "الرمز",
270
+ "resend": "أرسل رمزًا جديدًا",
271
+ "resent": "أرسلنا رمزًا جديدًا.",
272
+ "changeEmail": "استخدم بريدًا آخر",
273
+ "errors": {
274
+ "codeInvalid": "الرمز غير صحيح أو انتهت صلاحيته.",
275
+ "tooManyAttempts": "رموز خاطئة كثيرة. اطلب رمزًا جديدًا.",
276
+ "expired": "استغرق الأمر وقتًا طويلًا. ابدأ من جديد.",
277
+ "unavailable": "لا يستطيع Oxy إرسال البريد الآن. حاول لاحقًا.",
278
+ "rateLimited": "رموز كثيرة. حاول لاحقًا."
279
+ }
280
+ },
281
+ "deleteAccount": {
282
+ "passkey": {
283
+ "subtitle": "أكّد بمفتاح المرور. سيُحذف @{{username}} وكل ما فيه نهائيًا.",
284
+ "action": "الحذف بمفتاح المرور",
285
+ "done": "حُذف حسابك.",
286
+ "closeWindow": "يمكنك إغلاق هذه النافذة.",
287
+ "commons": "يستخدم هذا الحساب Commons: احذفه في Oxy Commons من الإعدادات > حذف الحساب، بمفتاحه."
288
+ }
289
+ },
290
+ "linkCommons": {
291
+ "title": "اربط Commons",
292
+ "subtitle": "احتفظ بمفتاحك: اربط Commons ليصبح هذا الحساب لك وحدك. يُحذف بريد الاسترداد الخاص به، وتصبح عبارة الاسترداد في Commons طريقك للعودة.",
293
+ "scan": "على هاتفك، افتح Commons واختر «لدي حساب Oxy على الويب» ثم امسح هذا الرمز.",
294
+ "waiting": "بانتظار Commons…",
295
+ "compareTitle": "تحقق من الرمز",
296
+ "compare": "يعرض Commons الرمز نفسه. إن لم يكن كذلك، ألغِ العملية.",
297
+ "confirm": "نعم، اربط Commons",
298
+ "cancel": "إلغاء",
299
+ "doneTitle": "تم ربط Commons",
300
+ "done": "أصبح هذا الحساب ذاتي الحفظ. حُذف بريد الاسترداد الخاص به: عبارة الاسترداد في Commons هي طريقك للعودة.",
301
+ "already": "يستخدم هذا الحساب Commons بالفعل.",
302
+ "expired": "انتهت صلاحية هذا الرمز.",
303
+ "renew": "اعرض رمزًا جديدًا",
304
+ "failed": "تعذّر ربط Commons. حاول مجددًا."
230
305
  }
231
306
  }
@@ -50,7 +50,26 @@
50
50
  "createInCommons": "Crea la teva identitat a Commons",
51
51
  "createAccount": "Crear compte",
52
52
  "backToSignInLink": "Ja tens compte? Inicia sessió",
53
- "webSubtitle": "Sense contrasenya. El teu dispositiu crea una passkey i el teu compte rep una frase de recuperació que només guardes tu."
53
+ "webSubtitle": "Sense contrasenya: el teu dispositiu crea una passkey i un correu de recuperació et torna l'accés si la perds.",
54
+ "username": {
55
+ "label": "Nom d'usuari",
56
+ "placeholder": "elteunom",
57
+ "required": "Tria un nom d'usuari.",
58
+ "taken": "Aquest nom d'usuari ja està agafat."
59
+ },
60
+ "email": {
61
+ "title": "El teu correu de recuperació",
62
+ "subtitle": "Si perds la passkey, t'hi enviem un codi per tornar a entrar. No s'utilitza per a res més.",
63
+ "label": "Correu",
64
+ "placeholder": "tu@exemple.com",
65
+ "invalid": "Escriu un correu vàlid."
66
+ },
67
+ "passkey": {
68
+ "title": "Crea la teva passkey",
69
+ "subtitle": "Hi inicies la sessió: l'empremta, la cara o el bloqueig de pantalla. No cal recordar res.",
70
+ "action": "Crea la passkey"
71
+ },
72
+ "commonsInstead": "Vols guardar la teva pròpia clau? Fes servir Commons"
54
73
  },
55
74
  "accountSwitcher": {
56
75
  "loading": "S'estan carregant els comptes...",
@@ -225,7 +244,63 @@
225
244
  "help": "Ajuda i comentaris",
226
245
  "signOut": "Tancar sessió",
227
246
  "privacy": "Política de privacitat",
228
- "terms": "Condicions del servei",
229
- "identity": "La teva identitat"
247
+ "terms": "Condicions del servei"
248
+ },
249
+ "recover": {
250
+ "title": "Recupera el compte",
251
+ "subtitle": "Escriu el nom d'usuari o el correu de recuperació. Enviarem un codi al correu de recuperació del compte.",
252
+ "identifier": {
253
+ "label": "Usuari o correu",
254
+ "placeholder": "elteunom",
255
+ "required": "Escriu l'usuari o el correu."
256
+ },
257
+ "commonsNote": "El teu compte fa servir Commons? Recupera-lo a Commons amb la frase de recuperació.",
258
+ "passkey": {
259
+ "title": "Afegeix una passkey nova",
260
+ "subtitle": "Per a @{{username}}. Treu les passkeys perdudes més tard a la configuració de seguretat del compte.",
261
+ "action": "Crea la passkey"
262
+ },
263
+ "backToSignIn": "Torna a iniciar la sessió"
264
+ },
265
+ "emailCode": {
266
+ "title": "Mira el correu",
267
+ "sentTo": "Hem enviat un codi de 6 xifres a {{email}}.",
268
+ "sentIfAccount": "Si aquest compte té un correu de recuperació, li hem enviat un codi de 6 xifres.",
269
+ "label": "Codi",
270
+ "resend": "Envia un codi nou",
271
+ "resent": "T'hem enviat un codi nou.",
272
+ "changeEmail": "Fes servir un altre correu",
273
+ "errors": {
274
+ "codeInvalid": "Aquest codi no és correcte o ha caducat.",
275
+ "tooManyAttempts": "Massa codis incorrectes. Demana-ne un de nou.",
276
+ "expired": "Ha passat massa temps. Torna a començar.",
277
+ "unavailable": "Oxy no pot enviar correus ara. Torna-ho a provar més tard.",
278
+ "rateLimited": "Massa codis. Torna-ho a provar més tard."
279
+ }
280
+ },
281
+ "deleteAccount": {
282
+ "passkey": {
283
+ "subtitle": "Confirma amb la passkey. @{{username}} i tot el que conté s'eliminen per sempre.",
284
+ "action": "Elimina amb passkey",
285
+ "done": "El teu compte s'ha eliminat.",
286
+ "closeWindow": "Pots tancar aquesta finestra.",
287
+ "commons": "Aquest compte fa servir Commons: elimina'l a Oxy Commons, Configuració > Elimina el compte, amb la seva clau."
288
+ }
289
+ },
290
+ "linkCommons": {
291
+ "title": "Vincula Commons",
292
+ "subtitle": "Guarda la teva pròpia clau: vincula Commons i aquest compte serà només teu. El seu correu de recuperació s'elimina, i la frase de recuperació de Commons és com hi tornes a entrar.",
293
+ "scan": "Al mòbil, obre Commons, tria «Tinc un compte d'Oxy al web» i escaneja aquest codi.",
294
+ "waiting": "Esperant Commons…",
295
+ "compareTitle": "Comprova el codi",
296
+ "compare": "Commons mostra el mateix codi. Si no és així, cancel·la.",
297
+ "confirm": "Sí, vincula Commons",
298
+ "cancel": "Cancel·la",
299
+ "doneTitle": "Commons està vinculat",
300
+ "done": "Aquest compte ara és d'autocustòdia. El seu correu de recuperació s'ha eliminat: la frase de recuperació de Commons és com hi tornes a entrar.",
301
+ "already": "Aquest compte ja fa servir Commons.",
302
+ "expired": "Aquest codi ha caducat.",
303
+ "renew": "Mostra un codi nou",
304
+ "failed": "No s'ha pogut vincular Commons. Torna-ho a provar."
230
305
  }
231
306
  }
@@ -50,7 +50,26 @@
50
50
  "createInCommons": "Erstellen Sie Ihre Identität in Commons",
51
51
  "createAccount": "Konto erstellen",
52
52
  "backToSignInLink": "Sie haben bereits ein Konto? Anmelden",
53
- "webSubtitle": "Kein Passwort. Dein Gerät erstellt einen Passkey, und dein Konto erhält eine Wiederherstellungsphrase, die nur du aufbewahrst."
53
+ "webSubtitle": "Kein Passwort: Dein Gerät erstellt einen Passkey, und eine Wiederherstellungs-E-Mail bringt dich zurück, wenn du ihn verlierst.",
54
+ "username": {
55
+ "label": "Benutzername",
56
+ "placeholder": "deinname",
57
+ "required": "Wähle einen Benutzernamen.",
58
+ "taken": "Dieser Benutzername ist vergeben."
59
+ },
60
+ "email": {
61
+ "title": "Deine Wiederherstellungs-E-Mail",
62
+ "subtitle": "Wenn du deinen Passkey verlierst, schicken wir dir hierhin einen Code, um wieder hineinzukommen. Sonst wird sie für nichts verwendet.",
63
+ "label": "E-Mail",
64
+ "placeholder": "du@beispiel.de",
65
+ "invalid": "Gib eine gültige E-Mail-Adresse ein."
66
+ },
67
+ "passkey": {
68
+ "title": "Erstelle deinen Passkey",
69
+ "subtitle": "Damit meldest du dich an: Fingerabdruck, Gesicht oder Bildschirmsperre. Nichts zu merken.",
70
+ "action": "Passkey erstellen"
71
+ },
72
+ "commonsInstead": "Willst du deinen eigenen Schlüssel behalten? Nutze stattdessen Commons"
54
73
  },
55
74
  "accountSwitcher": {
56
75
  "loading": "Konten werden geladen...",
@@ -225,7 +244,63 @@
225
244
  "help": "Hilfe & Feedback",
226
245
  "signOut": "Abmelden",
227
246
  "privacy": "Datenschutzerklärung",
228
- "terms": "Nutzungsbedingungen",
229
- "identity": "Ihre Identität"
247
+ "terms": "Nutzungsbedingungen"
248
+ },
249
+ "recover": {
250
+ "title": "Konto wiederherstellen",
251
+ "subtitle": "Gib deinen Benutzernamen oder deine Wiederherstellungs-E-Mail ein. Wir schicken einen Code an die Wiederherstellungs-E-Mail des Kontos.",
252
+ "identifier": {
253
+ "label": "Benutzername oder E-Mail",
254
+ "placeholder": "deinname",
255
+ "required": "Gib deinen Benutzernamen oder deine E-Mail ein."
256
+ },
257
+ "commonsNote": "Nutzt dein Konto Commons? Stelle es in Commons mit deiner Wiederherstellungsphrase wieder her.",
258
+ "passkey": {
259
+ "title": "Neuen Passkey hinzufügen",
260
+ "subtitle": "Für @{{username}}. Entferne verlorene Passkeys später in den Sicherheitseinstellungen deines Kontos.",
261
+ "action": "Passkey erstellen"
262
+ },
263
+ "backToSignIn": "Zurück zur Anmeldung"
264
+ },
265
+ "emailCode": {
266
+ "title": "Sieh in deine E-Mails",
267
+ "sentTo": "Wir haben einen 6-stelligen Code an {{email}} geschickt.",
268
+ "sentIfAccount": "Wenn dieses Konto eine Wiederherstellungs-E-Mail hat, haben wir ihr einen 6-stelligen Code geschickt.",
269
+ "label": "Code",
270
+ "resend": "Neuen Code senden",
271
+ "resent": "Wir haben einen neuen Code geschickt.",
272
+ "changeEmail": "Andere E-Mail verwenden",
273
+ "errors": {
274
+ "codeInvalid": "Dieser Code stimmt nicht oder ist abgelaufen.",
275
+ "tooManyAttempts": "Zu viele falsche Codes. Fordere einen neuen an.",
276
+ "expired": "Das hat zu lange gedauert. Fang noch einmal an.",
277
+ "unavailable": "Oxy kann gerade keine E-Mails senden. Versuch es später noch einmal.",
278
+ "rateLimited": "Zu viele Codes. Versuch es später noch einmal."
279
+ }
280
+ },
281
+ "deleteAccount": {
282
+ "passkey": {
283
+ "subtitle": "Bestätige mit deinem Passkey. @{{username}} und alles darin werden endgültig gelöscht.",
284
+ "action": "Mit Passkey löschen",
285
+ "done": "Dein Konto ist gelöscht.",
286
+ "closeWindow": "Du kannst dieses Fenster schließen.",
287
+ "commons": "Dieses Konto nutzt Commons: Lösche es in Oxy Commons unter Einstellungen > Konto löschen, mit seinem Schlüssel."
288
+ }
289
+ },
290
+ "linkCommons": {
291
+ "title": "Commons verknüpfen",
292
+ "subtitle": "Behalte deinen eigenen Schlüssel: Verknüpfe Commons, und dieses Konto gehört nur dir. Seine Wiederherstellungs-E-Mail wird gelöscht, und deine Wiederherstellungsphrase in Commons bringt dich zurück.",
293
+ "scan": "Öffne Commons auf deinem Handy, wähle „Ich habe ein Oxy-Konto im Web“ und scanne diesen Code.",
294
+ "waiting": "Warte auf Commons…",
295
+ "compareTitle": "Prüfe den Code",
296
+ "compare": "Commons zeigt denselben Code. Wenn nicht, brich ab.",
297
+ "confirm": "Ja, Commons verknüpfen",
298
+ "cancel": "Abbrechen",
299
+ "doneTitle": "Commons ist verknüpft",
300
+ "done": "Dieses Konto ist jetzt selbstverwahrt. Seine Wiederherstellungs-E-Mail wurde gelöscht: Deine Wiederherstellungsphrase in Commons bringt dich zurück.",
301
+ "already": "Dieses Konto nutzt bereits Commons.",
302
+ "expired": "Dieser Code ist abgelaufen.",
303
+ "renew": "Neuen Code anzeigen",
304
+ "failed": "Commons konnte nicht verknüpft werden. Versuch es noch einmal."
230
305
  }
231
306
  }