@volter/twin-xidentity 0.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.
Files changed (52) hide show
  1. package/README.md +112 -0
  2. package/client/xidentity-consent.css +204 -0
  3. package/client/xidentity-consent.tsx +162 -0
  4. package/dist/client/xidentity-consent.bundle.js +235 -0
  5. package/dist/client/xidentity-consent.css +204 -0
  6. package/dist/client/xidentity-consent.d.ts +53 -0
  7. package/dist/client/xidentity-consent.js +57 -0
  8. package/dist/client/xidentity-consent.tsx +162 -0
  9. package/dist/src/cli.d.ts +2 -0
  10. package/dist/src/cli.js +44 -0
  11. package/dist/src/index.d.ts +15 -0
  12. package/dist/src/index.js +105 -0
  13. package/dist/src/xidentity-budget.d.ts +50 -0
  14. package/dist/src/xidentity-budget.js +108 -0
  15. package/dist/src/xidentity-capabilities.d.ts +3 -0
  16. package/dist/src/xidentity-capabilities.js +905 -0
  17. package/dist/src/xidentity-conformance.d.ts +10 -0
  18. package/dist/src/xidentity-conformance.js +332 -0
  19. package/dist/src/xidentity-connector.d.ts +84 -0
  20. package/dist/src/xidentity-connector.js +239 -0
  21. package/dist/src/xidentity-consent-client.gen.d.ts +2 -0
  22. package/dist/src/xidentity-consent-client.gen.js +10 -0
  23. package/dist/src/xidentity-consent-ui.d.ts +21 -0
  24. package/dist/src/xidentity-consent-ui.js +94 -0
  25. package/dist/src/xidentity-pkce.d.ts +7 -0
  26. package/dist/src/xidentity-pkce.js +27 -0
  27. package/dist/src/xidentity-problems.d.ts +38 -0
  28. package/dist/src/xidentity-problems.js +108 -0
  29. package/dist/src/xidentity-scopes.d.ts +23 -0
  30. package/dist/src/xidentity-scopes.js +81 -0
  31. package/dist/src/xidentity-server.d.ts +33 -0
  32. package/dist/src/xidentity-server.js +85 -0
  33. package/dist/src/xidentity-store.d.ts +97 -0
  34. package/dist/src/xidentity-store.js +358 -0
  35. package/dist/src/xidentity-twin.d.ts +54 -0
  36. package/dist/src/xidentity-twin.js +851 -0
  37. package/package.json +74 -0
  38. package/src/cli.ts +43 -0
  39. package/src/index.ts +177 -0
  40. package/src/xidentity-budget.ts +135 -0
  41. package/src/xidentity-capabilities.ts +1012 -0
  42. package/src/xidentity-conformance.ts +370 -0
  43. package/src/xidentity-connector.ts +269 -0
  44. package/src/xidentity-consent-client.gen.ts +10 -0
  45. package/src/xidentity-consent-ui.ts +113 -0
  46. package/src/xidentity-journey.uitest.ts +277 -0
  47. package/src/xidentity-pkce.ts +29 -0
  48. package/src/xidentity-problems.ts +128 -0
  49. package/src/xidentity-scopes.ts +96 -0
  50. package/src/xidentity-server.ts +97 -0
  51. package/src/xidentity-store.ts +419 -0
  52. package/src/xidentity-twin.ts +944 -0
@@ -0,0 +1,358 @@
1
+ // X identity twin — STATE. Everything the twin knows lives in the shared kernel action log (D1):
2
+ // OAuth clients, X accounts, the browser session (which account is "logged in" at x.com), pending
3
+ // authorization requests, authorization codes, access/refresh tokens, the per-(client, user) grant
4
+ // record and the per-user rate window. There is no parallel store.
5
+ //
6
+ // ID MINTING (ADDING_A_TWIN.md §5): every id is either the vendor's own natural key (an account
7
+ // id) or MINTED HERE. The minted ones used to be ENTROPIC (`randomUUID`) — never a module counter
8
+ // and never a row count. Every one of them is also SERVED: the `ar_` authorize handle is rendered
9
+ // into the screen, the `…:ac:1` code rides the callback redirect, the `…:at:1`/`…:rt:1` tokens
10
+ // ride the /2/oauth2/token reply, a minted client id rides the twin door's reply. Runtime contract
11
+ // R9 (a served response is a pure function of (request, stored state) — no clock and no randomness
12
+ // in served content) therefore governs all of them, and entropy is exactly the defect it names, so
13
+ // they are ALL minted the resend way now: a stable hash of the world instant + the number of rows
14
+ // of that type the store already holds (soft-deleted rows counted, so a value can never recur) +
15
+ // the subject it is issued for.
16
+ //
17
+ // §5's count-mint hazard — a pulled vendor id sitting in a GAP above the row count, which local
18
+ // creates then walk into and clobber — cannot arise here: these types are twin-internal or
19
+ // twin-issued, no connector pulls one, and the count is a hash SEED rather than the id itself.
20
+ // What §5's entropy bought (two writes at one instant never colliding) the count buys instead: it
21
+ // advances on every mint.
22
+ //
23
+ // The `ar_` handle carries one extra rule the credentials do not: an authorize request whose
24
+ // parameters already name an UNSETTLED pending row reuses that row's id, so re-serving the same
25
+ // authorize screen is byte-identical (and a reload does not pile up a second pending request).
26
+ //
27
+ // The mutable-in-place rows (a code being consumed, a token being revoked, a grant re-granted, a
28
+ // rate window advancing) carry a per-subject `rev` ordinal (the upstash precedent) so a
29
+ // genuine write can never be mistaken for a replay.
30
+ import { applyTwinWrite, applyTwinWriteAtomic, projectResources } from '@volter/world-core';
31
+ export const SERVICE = 'xidentity';
32
+ export const RESOURCE_TYPES = [
33
+ 'oauth_client',
34
+ 'account',
35
+ 'session',
36
+ 'auth_request',
37
+ 'authorization_code',
38
+ 'access_token',
39
+ 'refresh_token',
40
+ 'grant',
41
+ 'rate_window',
42
+ ];
43
+ export function readAll(root) {
44
+ return projectResources(SERVICE, root);
45
+ }
46
+ export function readType(root, type) {
47
+ return readAll(root).filter((r) => r.type === type);
48
+ }
49
+ export function readOne(root, type, id) {
50
+ return readAll(root).find((r) => r.type === type && r.id === id);
51
+ }
52
+ /** The next `rev` for a subject that is being rewritten in place (survives deletes/tombstones). */
53
+ export function nextRev(root, type, id) {
54
+ const existing = readOne(root, type, id);
55
+ return (typeof existing?.rev === 'number' ? existing.rev : 0) + 1;
56
+ }
57
+ /** The next revision from a snapshot already held under the kernel projection lock. */
58
+ export function nextRevIn(resources, type, id) {
59
+ const existing = resources.find((r) => r.type === type && r.id === id);
60
+ return (typeof existing?.rev === 'number' ? existing.rev : 0) + 1;
61
+ }
62
+ /** A state-dependent one-row update whose read/revision/write decision is one kernel transaction. */
63
+ export async function writeAtomic(type, id, operation, fields, opts) {
64
+ await applyTwinWriteAtomic(SERVICE, (resources) => ({
65
+ kind: 'write',
66
+ value: undefined,
67
+ write: {
68
+ operation,
69
+ subjectType: type,
70
+ subjectId: id,
71
+ fields: { ...fields(resources), rev: nextRevIn(resources, type, id) },
72
+ ...(opts.occurredAt ? { occurredAt: opts.occurredAt } : {}),
73
+ actor: { kind: 'agent' },
74
+ },
75
+ }), opts.root);
76
+ return readOne(opts.root, type, id);
77
+ }
78
+ export async function write(type, id, operation, fields, opts) {
79
+ const { resource } = await applyTwinWrite(SERVICE, {
80
+ operation,
81
+ subjectType: type,
82
+ subjectId: id,
83
+ fields,
84
+ ...(opts.occurredAt ? { occurredAt: opts.occurredAt } : {}),
85
+ actor: { kind: 'agent' },
86
+ }, opts.root);
87
+ return resource;
88
+ }
89
+ // ── vendor-shaped id minting ────────────────────────────────────────────────────────────────────
90
+ // X's OAuth 2.0 artefacts are base64url blobs with an internal structure the vendor's own docs
91
+ // examples expose when decoded (docs.x.com user-access-token guide, fetched 2026-08-21):
92
+ //
93
+ // client_id M1M5R3BMVy13QmpScXkzTUt5OE46MTpjaQ → "<21 alnum>:1:ci"
94
+ // auth code VGNibzFWSWRE…RchOXlMd2dxOjE2MjIxNjA4MjU4MjU6MToxOmFjOjE
95
+ // → "<opaque>:<unix ms>:1:1:ac:1"
96
+ // access token Q0Mzb0VhZ0V5…U3NZQnZQYjoxNjIyMTQ3NzQzOTE0OjE6MTphdDox
97
+ // → "<opaque>:<unix ms>:1:1:at:1"
98
+ // refresh token bWRWa3gzdnk3…QVdxbm06MTYyMjE0Nzc0MzkxNDoxOjE6cnQ6MQ
99
+ // → "<opaque>:<unix ms>:1:1:rt:1"
100
+ //
101
+ // The twin reproduces the SHAPE (an integrator's code may key on it); the bytes are not the
102
+ // vendor's.
103
+ //
104
+ // Those bytes used to be `randomUUID()` entropy, and every one of them is SERVED (the code rides
105
+ // the callback redirect, the tokens ride the /2/oauth2/token reply), so R9 governs them exactly as
106
+ // it governs the `ar_` handle below. They are the resend pattern now: `stableHex` over (type, the
107
+ // world instant, the rows of that type held BEFORE the insert, the subject being issued for),
108
+ // occupying the same 21/45 characters of the same hex alphabet the entropic version did — the
109
+ // shape is byte-for-byte what it was.
110
+ //
111
+ // A twin's credentials are stand-ins, not secrets: determinism is the contract, and predictability
112
+ // from public state is not a threat model a twin has. UNIQUENESS is, and the seed carries it: the
113
+ // row COUNT advances on every mint, so two credentials of one type at one instant differ even when
114
+ // the subject is identical, and the SUBJECT is the credential being exchanged — the consent screen
115
+ // for a code, the code or spent refresh token for a token — so concurrent exchanges never meet.
116
+ // `xidentity.refresh.rotation` (a rotation must hand back a refresh token the caller did not send)
117
+ // holds by construction: the spent row is already in the store when the replacement is seeded.
118
+ const b64url = (s) => Buffer.from(s, 'utf8').toString('base64url');
119
+ /** The seed every credential is minted from. `readType` is unfiltered (soft-deletes counted), so a
120
+ * count — and therefore a credential — can never recur for one (type, instant, subject). */
121
+ function credentialSeed(root, type, instant, subject) {
122
+ return `${type}:${instant}:${readType(root, type).length}:${subject}`;
123
+ }
124
+ /** An X OAuth 2.0 client id: base64url of `<opaque>:1:ci`. */
125
+ export const mintClientId = (root, instant) => b64url(`${stableHex(credentialSeed(root, 'oauth_client', instant, 'client'), 21)}:1:ci`);
126
+ /** An X OAuth 2.0 authorization code (`…:ac:1`). `subject` is the authorize screen it settles. */
127
+ export const mintAuthorizationCode = (root, at, subject) => b64url(`${stableHex(credentialSeed(root, 'authorization_code', at, subject), 45)}:${at}:1:1:ac:1`);
128
+ /** An X OAuth 2.0 user access token (`…:at:1`). `subject` is the credential exchanged for it. */
129
+ export const mintAccessToken = (root, at, subject) => b64url(`${stableHex(credentialSeed(root, 'access_token', at, subject), 45)}:${at}:1:1:at:1`);
130
+ /** An X OAuth 2.0 refresh token (`…:rt:1`). `subject` is the credential exchanged for it. */
131
+ export const mintRefreshToken = (root, at, subject) => b64url(`${stableHex(credentialSeed(root, 'refresh_token', at, subject), 45)}:${at}:1:1:rt:1`);
132
+ // ── the authorize screen's handle: DETERMINISTIC, because it is SERVED (R9) ─────────────────────
133
+ /** FNV-1a over the seed, widened by re-hashing with a round counter until `n` hex chars exist
134
+ * (the resend exemplar's `stableHex`). Pure: no clock, no entropy, no host state. */
135
+ function stableHex(seed, n) {
136
+ let out = '';
137
+ for (let round = 0; out.length < n; round += 1) {
138
+ let h = 0x811c9dc5;
139
+ const s = `${round}:${seed}`;
140
+ for (let i = 0; i < s.length; i += 1) {
141
+ h ^= s.charCodeAt(i);
142
+ h = Math.imul(h, 0x01000193) >>> 0;
143
+ }
144
+ out += h.toString(16).padStart(8, '0');
145
+ }
146
+ return out.slice(0, n);
147
+ }
148
+ /** Every authorize parameter an `auth_request` row holds — its stable identity. Read off the
149
+ * fields being written OR off a projected row (the key set is the same on both). */
150
+ const AUTH_REQUEST_SIG_KEYS = [
151
+ 'clientId', 'redirectUri', 'scope', 'state', 'codeChallenge', 'codeChallengeMethod', 'accountId',
152
+ ];
153
+ function authRequestSignature(fields) {
154
+ return AUTH_REQUEST_SIG_KEYS.map((k) => `${k}=${String(fields[k] ?? '')}`).join('\u0001');
155
+ }
156
+ /**
157
+ * Twin-internal handle for an authorize screen in flight (never leaves the twin's own pages) —
158
+ * `ar_` + 32 hex, the shape `randomUUID()` gave it, now a pure function of (request, stored state).
159
+ *
160
+ * An authorize request whose parameters ALREADY name an unsettled pending row reuses that row's
161
+ * id: pressing reload on the authorize screen is the same screen, not a new one. Otherwise the id
162
+ * is `stableHex(auth_request:<world instant>:<rows already held>)` — two authorize requests at one
163
+ * instant differ by the count, and two identical worlds mint identical ids.
164
+ */
165
+ export function authRequestIdFor(root, occurredAt, fields) {
166
+ const prior = readType(root, 'auth_request');
167
+ const signature = authRequestSignature(fields);
168
+ const reusable = prior.find((r) => r.settled !== true && authRequestSignature(r) === signature);
169
+ if (reusable)
170
+ return String(reusable.id);
171
+ return `ar_${stableHex(`auth_request:${occurredAt}:${prior.length}`, 32)}`;
172
+ }
173
+ // ── seeded world ────────────────────────────────────────────────────────────────────────────────
174
+ // The authorize page needs SOMEBODY to be signed in at x.com. X shows the consent screen for the
175
+ // browser's current session; the twin's equivalent is deterministic personas seeded on first use
176
+ // plus a `session` row naming the active one (`POST /_twin/session` switches it — X's real switcher
177
+ // is the x.com account menu, which is not OAuth surface). The default clients mirror the two shapes
178
+ // the X developer portal hands out: a CONFIDENTIAL client (Web App — has a secret, authenticates
179
+ // with HTTP Basic) and a PUBLIC client (Native/SPA — no secret, client_id travels in the body).
180
+ export const DEFAULT_CLIENT_ID = 'VHdpbkRlbW9BcHAwMDAwMDAwMDA6MTpjaQ';
181
+ export const DEFAULT_CLIENT_SECRET = 'twin-confidential-secret-0000000000000000000000000';
182
+ export const DEFAULT_PUBLIC_CLIENT_ID = 'VHdpblB1YmxpY0FwcDAwMDAwMDA6MTpjaQ';
183
+ /** X user ids are stable numeric strings (the docs' own example user is "2244994945"). The seeded
184
+ * ids start at 9e18 — vendor-SHAPED (int64-range numeric) but far ABOVE the live snowflake id
185
+ * space (~1.9e18 in 2026), so a locally seeded persona can never collide with a pulled real id
186
+ * (the datadog EVENT_ID_BASE precedent; §9 round one). */
187
+ export const DEFAULT_ACCOUNTS = [
188
+ {
189
+ id: '9000000000000000001',
190
+ username: 'ada_twin',
191
+ name: 'Ada Lovelace',
192
+ createdAt: '2013-12-14T04:35:55.000Z',
193
+ description: 'Analyst. Engine programmer. First of her kind.',
194
+ location: 'London',
195
+ protectedAccount: false,
196
+ verified: true,
197
+ verifiedType: 'blue',
198
+ publicMetrics: { followers_count: 5834, following_count: 204, tweet_count: 1405, listed_count: 16, like_count: 320, media_count: 12 },
199
+ url: 'https://ada.example',
200
+ confirmedEmail: 'ada@twin.example',
201
+ },
202
+ {
203
+ id: '9000000000000000002',
204
+ username: 'grace_twin',
205
+ name: 'Grace Hopper',
206
+ createdAt: '2015-06-01T12:00:00.000Z',
207
+ description: 'It is easier to ask forgiveness than permission.',
208
+ protectedAccount: true,
209
+ verified: false,
210
+ verifiedType: 'none',
211
+ publicMetrics: { followers_count: 120, following_count: 88, tweet_count: 5210, listed_count: 3, like_count: 990, media_count: 41 },
212
+ },
213
+ ];
214
+ /** NextAuth's callback path for the X provider — the one every X integration guide walks through. */
215
+ const CALLBACK_PATH = '/api/auth/callback/x';
216
+ /**
217
+ * Every spelling of the ORIGIN this twin was reached on that a browser could hand back.
218
+ *
219
+ * X matches redirect URIs EXACTLY and documents NO loopback-port exception (see
220
+ * `redirectUriAllowed` below), so `localhost` and the loopback IP are two different
221
+ * registrations — while a local app is reachable as both on the same port. A demo registration
222
+ * naming only one of them would fail half the time for a reason the developer cannot see.
223
+ */
224
+ function originSpellings(origin) {
225
+ const base = (origin ?? '').trim().replace(/\/+$/, '');
226
+ if (!base)
227
+ return [];
228
+ let u;
229
+ try {
230
+ u = new URL(base);
231
+ }
232
+ catch {
233
+ return [base];
234
+ }
235
+ if (u.protocol !== 'http:')
236
+ return [base];
237
+ const port = u.port ? `:${u.port}` : '';
238
+ if (u.hostname === 'localhost')
239
+ return [base, `http://127.0.0.1${port}`];
240
+ if (u.hostname === '127.0.0.1')
241
+ return [base, `http://localhost${port}`];
242
+ return [base];
243
+ }
244
+ /**
245
+ * The redirect URIs the seeded demo clients register.
246
+ *
247
+ * Runtime contract R7: a twin NEVER bakes a port into served content or config. The callbacks are
248
+ * DERIVED from the origin the twin was actually reached on (`XIdentityRequest.origin`, which
249
+ * xidentity-server fills from the URL the request arrived at). A caller that declares no origin —
250
+ * an in-process call — seeds no callbacks at all and registers its own through the twin door.
251
+ */
252
+ export function defaultRedirectUris(origin) {
253
+ return originSpellings(origin).map((base) => `${base}${CALLBACK_PATH}`);
254
+ }
255
+ /**
256
+ * Materialise the default clients + accounts + session once per root. Idempotent by subject id:
257
+ * re-running it over a seeded root writes nothing new, and it NEVER overwrites an
258
+ * operator-registered client, account or session choice.
259
+ */
260
+ export async function ensureSeed(opts) {
261
+ await applyTwinWriteAtomic(SERVICE, (resources) => {
262
+ const has = (type, id) => resources.some((r) => r.type === type && r.id === id);
263
+ const missing = [];
264
+ if (!has('oauth_client', DEFAULT_CLIENT_ID))
265
+ missing.push({
266
+ type: 'oauth_client',
267
+ id: DEFAULT_CLIENT_ID,
268
+ fields: {
269
+ name: 'Twin Demo App',
270
+ secret: DEFAULT_CLIENT_SECRET,
271
+ clientType: 'confidential',
272
+ redirectUris: defaultRedirectUris(opts.origin),
273
+ rev: 1,
274
+ },
275
+ });
276
+ if (!has('oauth_client', DEFAULT_PUBLIC_CLIENT_ID))
277
+ missing.push({
278
+ type: 'oauth_client',
279
+ id: DEFAULT_PUBLIC_CLIENT_ID,
280
+ fields: {
281
+ name: 'Twin Public App',
282
+ secret: null,
283
+ clientType: 'public',
284
+ redirectUris: defaultRedirectUris(opts.origin),
285
+ rev: 1,
286
+ },
287
+ });
288
+ for (const account of DEFAULT_ACCOUNTS) {
289
+ if (has('account', account.id))
290
+ continue;
291
+ missing.push({
292
+ type: 'account',
293
+ id: account.id,
294
+ fields: {
295
+ username: account.username,
296
+ name: account.name,
297
+ createdAt: account.createdAt,
298
+ description: account.description,
299
+ ...(account.location ? { location: account.location } : {}),
300
+ ...(account.profileImageUrl ? { profileImageUrl: account.profileImageUrl } : {}),
301
+ protectedAccount: account.protectedAccount,
302
+ verified: account.verified,
303
+ verifiedType: account.verifiedType,
304
+ publicMetrics: account.publicMetrics,
305
+ ...(account.url ? { url: account.url } : {}),
306
+ ...(account.confirmedEmail ? { confirmedEmail: account.confirmedEmail } : {}),
307
+ rev: 1,
308
+ },
309
+ });
310
+ }
311
+ if (!has('session', 'current'))
312
+ missing.push({
313
+ type: 'session',
314
+ id: 'current',
315
+ fields: { accountId: DEFAULT_ACCOUNTS[0].id, rev: 1 },
316
+ });
317
+ if (missing.length === 0)
318
+ return { kind: 'skip', value: undefined };
319
+ const [primary, ...rest] = missing;
320
+ return {
321
+ kind: 'write',
322
+ value: undefined,
323
+ write: {
324
+ operation: 'seed.ensure',
325
+ subjectType: primary.type,
326
+ subjectId: primary.id,
327
+ fields: primary.fields,
328
+ projection: {
329
+ creates: rest.map((row) => ({ type: row.type, id: row.id, fields: row.fields })),
330
+ },
331
+ ...(opts.occurredAt ? { occurredAt: opts.occurredAt } : {}),
332
+ actor: { kind: 'system' },
333
+ },
334
+ };
335
+ }, opts.root);
336
+ }
337
+ /** The account the x.com browser session is signed in as (the one the authorize screen consents). */
338
+ export function sessionAccount(root) {
339
+ const session = readOne(root, 'session', 'current');
340
+ const id = typeof session?.accountId === 'string' ? session.accountId : undefined;
341
+ return id ? readOne(root, 'account', id) : undefined;
342
+ }
343
+ /**
344
+ * Is `candidate` an EXACT registered callback URI for this client? X documents exact-match
345
+ * validation for OAuth 2.0 callback URLs ("This value must correspond to one of the Callback URLs
346
+ * defined in your App's settings" — and the legacy official SDK's own docblock spells out "exact
347
+ * match validation"). No loopback-port exception is documented for X, so none is modelled — a
348
+ * loosely-matching twin would hide the single most common integration bug.
349
+ */
350
+ export function redirectUriAllowed(client, candidate) {
351
+ const registered = Array.isArray(client.redirectUris) ? client.redirectUris : [];
352
+ // A fragment never legitimately appears in a redirect_uri (RFC 6749 §3.1.2) and exact string
353
+ // membership would still admit one only if it had been REGISTERED with a fragment — refuse the
354
+ // candidate outright before comparing.
355
+ if (candidate.includes('#'))
356
+ return false;
357
+ return registered.includes(candidate);
358
+ }
@@ -0,0 +1,54 @@
1
+ import { type XResponse } from './xidentity-problems.js';
2
+ export { RESOURCE_TYPES } from './xidentity-store.js';
3
+ export type { XResponse } from './xidentity-problems.js';
4
+ /** X's real hosts. The authorize screen lives on the site host; the API on api.x.com. The legacy
5
+ * twitter.com pair still serves the same surface and X's legacy official SDK still targets it. */
6
+ export declare const AUTHORIZE_ORIGIN = "https://x.com";
7
+ export declare const API_ORIGIN = "https://api.x.com";
8
+ export declare const LEGACY_AUTHORIZE_ORIGIN = "https://twitter.com";
9
+ export declare const LEGACY_API_ORIGIN = "https://api.twitter.com";
10
+ /** X's documented default access-token lifetime: "Access tokens default to 2-hour validity"
11
+ * (docs.x.com authorization-code guide, fetched 2026-08-21). */
12
+ export declare const ACCESS_TOKEN_TTL_SECONDS = 7200;
13
+ /** X's authorization codes are short-lived; 30 seconds is the widely-reported figure.
14
+ * EXTRAPOLATION — not read from a fetched official artefact; pinned as
15
+ * `xidentity.token.code_ttl` (todo). The single-use rule, by contrast, is hard protocol. */
16
+ export declare const AUTH_CODE_TTL_SECONDS = 30;
17
+ /** GET /2/users/me: 75 requests / 15 min per user (docs.x.com/x-api/fundamentals/rate-limits,
18
+ * fetched 2026-08-21). */
19
+ export declare const USERS_ME_RATE_LIMIT = 75;
20
+ export declare const RATE_WINDOW_SECONDS: number;
21
+ export type XIdentityRequest = {
22
+ method: string;
23
+ /** Path plus query string, e.g. `/i/oauth2/authorize?client_id=…`. */
24
+ path: string;
25
+ body?: string;
26
+ /** Lower-cased request headers (authorization / content-type). */
27
+ headers?: Record<string, string>;
28
+ occurredAt?: string;
29
+ root?: string;
30
+ readOnly?: boolean;
31
+ /** Where this twin is reached (`twinPublicBase`: origin plus any served-World mount path) — the
32
+ * consent form's action must point back at the twin. Absent (an in-process call), vendor-real
33
+ * URLs are used. */
34
+ origin?: string;
35
+ /** The bare origin the request arrived at, when it differs from `origin` (a served World mounts
36
+ * the twin under a path). The seeded demo app's callbacks derive from it; defaults to `origin`. */
37
+ callbackOrigin?: string;
38
+ };
39
+ export type XIdentityResponse = XResponse;
40
+ /** The `user.fields` enum — the v2 OpenAPI's UserFieldsParameter (2.167), verbatim. */
41
+ export declare const USER_FIELDS: readonly ["confirmed_email", "connection_status", "created_at", "description", "entities", "id", "is_identity_verified", "location", "name", "parody", "profile_banner_url", "profile_image_url", "protected", "public_metrics", "receives_your_dm", "subscribes_to_you", "subscription", "subscription_type", "url", "username", "verified", "verified_followers_count", "verified_type", "withheld"];
42
+ /** The `expansions` enum — UserExpansionsParameter (2.167), verbatim. */
43
+ export declare const USER_EXPANSIONS: readonly ["affiliation", "most_recent_post_id", "pinned_post_id"];
44
+ /** The scopes GET /2/users/me demands — the OpenAPI operation's OAuth2UserToken entry lists BOTH. */
45
+ export declare const USERS_ME_REQUIRED_SCOPES: readonly ["tweet.read", "users.read"];
46
+ /** Every VENDOR endpoint this twin claims to serve. The conformance check drives one real request
47
+ * per entry and grades the OUTCOME, so an entry here is a promise with teeth. Twin-only routes
48
+ * (`/_twin/*`) are deliberately absent — they are scaffolding, not vendor surface. */
49
+ export declare function xIdentityTwinSnapshot(): {
50
+ implementedEndpoints: string[];
51
+ resourceTypes: readonly string[];
52
+ grantTypes: string[];
53
+ };
54
+ export declare function handleXIdentityTwinRequest(req: XIdentityRequest): Promise<XResponse>;