@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.
- package/README.md +112 -0
- package/client/xidentity-consent.css +204 -0
- package/client/xidentity-consent.tsx +162 -0
- package/dist/client/xidentity-consent.bundle.js +235 -0
- package/dist/client/xidentity-consent.css +204 -0
- package/dist/client/xidentity-consent.d.ts +53 -0
- package/dist/client/xidentity-consent.js +57 -0
- package/dist/client/xidentity-consent.tsx +162 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +44 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js +105 -0
- package/dist/src/xidentity-budget.d.ts +50 -0
- package/dist/src/xidentity-budget.js +108 -0
- package/dist/src/xidentity-capabilities.d.ts +3 -0
- package/dist/src/xidentity-capabilities.js +905 -0
- package/dist/src/xidentity-conformance.d.ts +10 -0
- package/dist/src/xidentity-conformance.js +332 -0
- package/dist/src/xidentity-connector.d.ts +84 -0
- package/dist/src/xidentity-connector.js +239 -0
- package/dist/src/xidentity-consent-client.gen.d.ts +2 -0
- package/dist/src/xidentity-consent-client.gen.js +10 -0
- package/dist/src/xidentity-consent-ui.d.ts +21 -0
- package/dist/src/xidentity-consent-ui.js +94 -0
- package/dist/src/xidentity-pkce.d.ts +7 -0
- package/dist/src/xidentity-pkce.js +27 -0
- package/dist/src/xidentity-problems.d.ts +38 -0
- package/dist/src/xidentity-problems.js +108 -0
- package/dist/src/xidentity-scopes.d.ts +23 -0
- package/dist/src/xidentity-scopes.js +81 -0
- package/dist/src/xidentity-server.d.ts +33 -0
- package/dist/src/xidentity-server.js +85 -0
- package/dist/src/xidentity-store.d.ts +97 -0
- package/dist/src/xidentity-store.js +358 -0
- package/dist/src/xidentity-twin.d.ts +54 -0
- package/dist/src/xidentity-twin.js +851 -0
- package/package.json +74 -0
- package/src/cli.ts +43 -0
- package/src/index.ts +177 -0
- package/src/xidentity-budget.ts +135 -0
- package/src/xidentity-capabilities.ts +1012 -0
- package/src/xidentity-conformance.ts +370 -0
- package/src/xidentity-connector.ts +269 -0
- package/src/xidentity-consent-client.gen.ts +10 -0
- package/src/xidentity-consent-ui.ts +113 -0
- package/src/xidentity-journey.uitest.ts +277 -0
- package/src/xidentity-pkce.ts +29 -0
- package/src/xidentity-problems.ts +128 -0
- package/src/xidentity-scopes.ts +96 -0
- package/src/xidentity-server.ts +97 -0
- package/src/xidentity-store.ts +419 -0
- package/src/xidentity-twin.ts +944 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ConsentView, type ErrorPageProps } from '../client/xidentity-consent.js';
|
|
2
|
+
export type { ConsentAccount, ConsentScopeRow, ConsentView, ErrorPageProps } from '../client/xidentity-consent.js';
|
|
3
|
+
export { XIDENTITY_CONSENT_CLIENT_CSS as CONSENT_CLIENT_CSS, XIDENTITY_CONSENT_CLIENT_JS as CONSENT_CLIENT_JS } from './xidentity-consent-client.gen.js';
|
|
4
|
+
/** Twin-only asset paths, clearly namespaced so they can never be mistaken for vendor surface. */
|
|
5
|
+
export declare const CONSENT_SCRIPT_PATH = "/_twin/assets/consent.js";
|
|
6
|
+
export declare const CONSENT_STYLE_PATH = "/_twin/assets/consent.css";
|
|
7
|
+
/**
|
|
8
|
+
* THE STATE BUILDER — the authorize screen's entire view model, folded out of the kernel
|
|
9
|
+
* projection. Returns `null` when the authorization request is unknown or already settled, or when
|
|
10
|
+
* no x.com session exists to consent as (the caller renders the error page); it never invents an
|
|
11
|
+
* app name, an account or a scope row.
|
|
12
|
+
*/
|
|
13
|
+
export declare function xIdentityConsentState(opts: {
|
|
14
|
+
root?: string;
|
|
15
|
+
requestId: string;
|
|
16
|
+
origin: string;
|
|
17
|
+
}): ConsentView | null;
|
|
18
|
+
/** Render the authorize page from a view model. The tab title mirrors X's "Authorize <app>". */
|
|
19
|
+
export declare function consentPageHtml(view: ConsentView, base?: string): string;
|
|
20
|
+
/** Render the authorize-endpoint error page (an un-redirectable failure). */
|
|
21
|
+
export declare function errorPageHtml(props: ErrorPageProps, base?: string): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// X identity twin — the SERVER HALF of the authorize screen.
|
|
2
|
+
//
|
|
3
|
+
// Named `-consent-ui` rather than `-mirror-ui` on purpose (the googleoauth class doctrine): a
|
|
4
|
+
// dashboard mirror is a SECOND renderer this repo writes over a vendor's data, whereas this is the
|
|
5
|
+
// vendor's OWN page, on the vendor's OWN path, in the middle of the vendor's OWN protocol. The
|
|
6
|
+
// mirror DISCIPLINE still applies in full — one renderer, data-coupled to the projection, driven
|
|
7
|
+
// by a real browser journey.
|
|
8
|
+
//
|
|
9
|
+
// The pieces:
|
|
10
|
+
// • `xIdentityConsentState` — THE STATE BUILDER. Everything on screen is derived here, from the
|
|
11
|
+
// kernel projection, and nowhere else. It is the seam `scripts/mutation-test.ts` sabotages in
|
|
12
|
+
// the mirror phase (TWIN-20/B9): with it dead the screen has no app name, no account and no
|
|
13
|
+
// scopes, so every UI capability must go red while the write/handler path stays real.
|
|
14
|
+
// • `consentPageHtml` / `errorPageHtml` — server-render the REAL exported React components with
|
|
15
|
+
// `renderToStaticMarkup`. There is no template string of vendor markup anywhere.
|
|
16
|
+
// • `CONSENT_CLIENT_JS` / `CONSENT_CLIENT_CSS` — the browser bundle and stylesheet as COMMITTED TEXT
|
|
17
|
+
// (`xidentity-consent-client.gen.ts`, written by `scripts/consent-clients.ts` and drift-gated):
|
|
18
|
+
// progressive enhancement only, and the serve path never builds (runtime contract R12b, R9).
|
|
19
|
+
import { createElement } from 'react';
|
|
20
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
21
|
+
import { ConsentPage, ErrorPage, } from "../client/xidentity-consent.js";
|
|
22
|
+
import { describeScopes, parseScopeParam, sortScopesForConsent } from "./xidentity-scopes.js";
|
|
23
|
+
import { readOne } from "./xidentity-store.js";
|
|
24
|
+
export { XIDENTITY_CONSENT_CLIENT_CSS as CONSENT_CLIENT_CSS, XIDENTITY_CONSENT_CLIENT_JS as CONSENT_CLIENT_JS } from "./xidentity-consent-client.gen.js";
|
|
25
|
+
/** Twin-only asset paths, clearly namespaced so they can never be mistaken for vendor surface. */
|
|
26
|
+
export const CONSENT_SCRIPT_PATH = '/_twin/assets/consent.js';
|
|
27
|
+
export const CONSENT_STYLE_PATH = '/_twin/assets/consent.css';
|
|
28
|
+
function toConsentAccount(row) {
|
|
29
|
+
return {
|
|
30
|
+
id: row.id,
|
|
31
|
+
username: String(row.username ?? ''),
|
|
32
|
+
name: String(row.name ?? ''),
|
|
33
|
+
profileImageUrl: String(row.profileImageUrl ?? ''),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* THE STATE BUILDER — the authorize screen's entire view model, folded out of the kernel
|
|
38
|
+
* projection. Returns `null` when the authorization request is unknown or already settled, or when
|
|
39
|
+
* no x.com session exists to consent as (the caller renders the error page); it never invents an
|
|
40
|
+
* app name, an account or a scope row.
|
|
41
|
+
*/
|
|
42
|
+
export function xIdentityConsentState(opts) {
|
|
43
|
+
const authRequest = readOne(opts.root, 'auth_request', opts.requestId);
|
|
44
|
+
if (!authRequest || authRequest.settled === true)
|
|
45
|
+
return null;
|
|
46
|
+
const client = readOne(opts.root, 'oauth_client', String(authRequest.clientId));
|
|
47
|
+
if (!client)
|
|
48
|
+
return null;
|
|
49
|
+
const account = readOne(opts.root, 'account', String(authRequest.accountId ?? ''));
|
|
50
|
+
if (!account)
|
|
51
|
+
return null;
|
|
52
|
+
const requested = sortScopesForConsent(parseScopeParam(String(authRequest.scope ?? '')));
|
|
53
|
+
const scopes = describeScopes(requested).map((s) => ({
|
|
54
|
+
scope: s.scope,
|
|
55
|
+
label: s.label,
|
|
56
|
+
group: s.group,
|
|
57
|
+
known: s.known,
|
|
58
|
+
}));
|
|
59
|
+
let redirectHost = '';
|
|
60
|
+
try {
|
|
61
|
+
redirectHost = new URL(String(authRequest.redirectUri)).host;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
redirectHost = String(authRequest.redirectUri);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
requestId: opts.requestId,
|
|
68
|
+
origin: opts.origin,
|
|
69
|
+
app: { clientId: client.id, name: String(client.name ?? '') },
|
|
70
|
+
account: toConsentAccount(account),
|
|
71
|
+
scopes,
|
|
72
|
+
redirectHost,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** `base` is where the twin is reached (`twinPublicBase`: origin plus any served-World mount path),
|
|
76
|
+
* so the page's assets resolve inside the World's mount; empty for an in-process render. */
|
|
77
|
+
function page(title, bodyMarkup, base) {
|
|
78
|
+
return `<!doctype html>
|
|
79
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
80
|
+
<title>${escapeHtml(title)}</title><link rel="stylesheet" href="${base}${CONSENT_STYLE_PATH}"></head>
|
|
81
|
+
<body>${bodyMarkup}<div id="consent-enhanced"></div><script type="module" src="${base}${CONSENT_SCRIPT_PATH}"></script></body></html>`;
|
|
82
|
+
}
|
|
83
|
+
function escapeHtml(s) {
|
|
84
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
85
|
+
}
|
|
86
|
+
/** Render the authorize page from a view model. The tab title mirrors X's "Authorize <app>". */
|
|
87
|
+
export function consentPageHtml(view, base = '') {
|
|
88
|
+
const markup = renderToStaticMarkup(createElement(ConsentPage, { view }));
|
|
89
|
+
return page(`Authorize ${view.app.name} to access your account? / X`, markup, base);
|
|
90
|
+
}
|
|
91
|
+
/** Render the authorize-endpoint error page (an un-redirectable failure). */
|
|
92
|
+
export function errorPageHtml(props, base = '') {
|
|
93
|
+
return page(`Error ${props.status}: ${props.code} / X`, renderToStaticMarkup(createElement(ErrorPage, props)), base);
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** base64url(SHA-256(verifier)) — the S256 code_challenge for a code_verifier. */
|
|
2
|
+
export declare function pkceS256(verifier: string): string;
|
|
3
|
+
export type ChallengeMethod = 'S256' | 'plain';
|
|
4
|
+
/** Normalise a caller-supplied code_challenge_method, or null when it isn't a documented value. */
|
|
5
|
+
export declare function normalizeChallengeMethod(raw: string | null): ChallengeMethod | null;
|
|
6
|
+
/** Does `verifier` satisfy the challenge the code was minted against? */
|
|
7
|
+
export declare function pkceVerifies(method: ChallengeMethod, challenge: string, verifier: string): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// PKCE (RFC 7636) — real SHA-256, no stubs. X's documented code_challenge_method values are
|
|
2
|
+
// `S256` and `plain` (docs.x.com authorization-code guide, fetched 2026-08-21).
|
|
3
|
+
//
|
|
4
|
+
// CASE: the docs write `S256`; X's LEGACY official SDK (twitter-api-typescript-sdk, OAuth2User.
|
|
5
|
+
// generateAuthURL) emits the lowercase `s256` in its authorize URLs, so a twin that refused the
|
|
6
|
+
// lowercase form would refuse the vendor's own client. Both spellings are accepted and normalised
|
|
7
|
+
// here; the exact live tolerance is pinned as a manifest todo rather than asserted
|
|
8
|
+
// (`xidentity.authorize.challenge_method_case`).
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
/** base64url(SHA-256(verifier)) — the S256 code_challenge for a code_verifier. */
|
|
11
|
+
export function pkceS256(verifier) {
|
|
12
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
13
|
+
}
|
|
14
|
+
/** Normalise a caller-supplied code_challenge_method, or null when it isn't a documented value. */
|
|
15
|
+
export function normalizeChallengeMethod(raw) {
|
|
16
|
+
if (raw === null)
|
|
17
|
+
return null;
|
|
18
|
+
if (raw === 'plain')
|
|
19
|
+
return 'plain';
|
|
20
|
+
if (raw.toUpperCase() === 'S256')
|
|
21
|
+
return 'S256';
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Does `verifier` satisfy the challenge the code was minted against? */
|
|
25
|
+
export function pkceVerifies(method, challenge, verifier) {
|
|
26
|
+
return method === 'S256' ? pkceS256(verifier) === challenge : verifier === challenge;
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type XResponse = {
|
|
2
|
+
status: number;
|
|
3
|
+
body: unknown;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
};
|
|
6
|
+
export declare function oauthError(error: string, description: string, status: number): XResponse;
|
|
7
|
+
/**
|
|
8
|
+
* The ONE answer an unusable authorization code gets — unknown, already-redeemed, expired, minted
|
|
9
|
+
* for another client, wrong redirect_uri pairing or failed PKCE are deliberately indistinguishable
|
|
10
|
+
* (telling them apart would leak which codes exist). The description is the widely-reported X wire
|
|
11
|
+
* string for this case; the `error` code X pairs with it is reported as `invalid_request` (where
|
|
12
|
+
* RFC 6749 would say `invalid_grant`) and the twin follows the REPORTED WIRE, marked extrapolated.
|
|
13
|
+
*/
|
|
14
|
+
export declare function badAuthorizationCode(): XResponse;
|
|
15
|
+
/** The refresh-token twin of the above — same reported-wire caveat. */
|
|
16
|
+
export declare function badRefreshToken(): XResponse;
|
|
17
|
+
/** Client authentication failed at the token/revoke endpoint (RFC 6749 §5.2: 401 invalid_client). */
|
|
18
|
+
export declare function invalidClient(): XResponse;
|
|
19
|
+
/** The generic auth problem X answers a bad/absent/expired bearer with on /2 resources. */
|
|
20
|
+
export declare function unauthorizedProblem(): XResponse;
|
|
21
|
+
/**
|
|
22
|
+
* A user token whose scope set does not cover the endpoint. The 403 + problem SHAPE follows the
|
|
23
|
+
* OpenAPI's default-error contract; X's exact wording for the insufficient-scope case was not
|
|
24
|
+
* captured from an official artefact, so the twin states the refusal plainly rather than inventing
|
|
25
|
+
* vendor prose — pinning the live body is `xidentity.users_me.insufficient_scope_wording` (todo).
|
|
26
|
+
*/
|
|
27
|
+
export declare function forbiddenProblem(detail: string): XResponse;
|
|
28
|
+
/** An unmodelled /2 route fails like the vendor: 404 problem, never a fake success. */
|
|
29
|
+
export declare function notFoundProblem(): XResponse;
|
|
30
|
+
/** A contained unexpected failure on the v2 resource surface. Never exposes local exception text. */
|
|
31
|
+
export declare function internalServerProblem(): XResponse;
|
|
32
|
+
/**
|
|
33
|
+
* The invalid-parameter envelope (OpenAPI InvalidRequestProblem + the wire's per-parameter
|
|
34
|
+
* `errors` array). `parameters` maps the offending query parameter to the values it arrived with.
|
|
35
|
+
*/
|
|
36
|
+
export declare function invalidRequestProblem(parameters: Record<string, string[]>, message: string): XResponse;
|
|
37
|
+
/** HTTP 429, legacy code 88 — the documented pair (docs.x.com fundamentals/rate-limits). */
|
|
38
|
+
export declare function rateLimitExceeded(headers: Record<string, string>): XResponse;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// X API v2 error envelopes — the vendor's THREE distinct error families, kept distinct because an
|
|
2
|
+
// integration can (and X's own SDKs do) branch on them:
|
|
3
|
+
//
|
|
4
|
+
// 1. OAuth token-endpoint errors: the RFC 6749 §5.2 flat body `{ error, error_description }`.
|
|
5
|
+
// The docs show no error examples (user-access-token guide, fetched 2026-08-21), so the
|
|
6
|
+
// `error` codes here are RFC 6749's and the descriptions are the widely-reported X wire
|
|
7
|
+
// strings — the EVIDENCE BOUNDARY is marked at each call site and the exact texts are filed
|
|
8
|
+
// as `xidentity.token.error_wording` (todo) rather than claimed pinned.
|
|
9
|
+
//
|
|
10
|
+
// 2. v2 problem envelopes (`application/problem+json` shapes): the OpenAPI 2.167 `Problem`
|
|
11
|
+
// family, discriminated by a `type` URL (`https://api.x.com/2/problems/…`). The generic
|
|
12
|
+
// auth failure is the `about:blank` form (`{title, type: "about:blank", status, detail}`) —
|
|
13
|
+
// observed wire behaviour for a bad bearer, marked extrapolated where the doc trail stops.
|
|
14
|
+
//
|
|
15
|
+
// 3. The request-level invalid-parameter envelope: `{errors: [{parameters, message}], title:
|
|
16
|
+
// "Invalid Request", detail: "One or more parameters to your request was invalid.", type:
|
|
17
|
+
// …/invalid-request}` — the OpenAPI's InvalidRequestProblem, with the per-parameter `errors`
|
|
18
|
+
// array the wire carries.
|
|
19
|
+
//
|
|
20
|
+
// 4. The 15-minute-window rate refusal: HTTP 429 with legacy error code 88 — docs.x.com
|
|
21
|
+
// fundamentals/rate-limits (fetched 2026-08-21) documents "a 429 error with error code 88:
|
|
22
|
+
// Rate limit exceeded" verbatim, hence the v1.1-style `{errors: [{code, message}]}` body.
|
|
23
|
+
const NOSTORE = { 'cache-control': 'no-cache, no-store, max-age=0' };
|
|
24
|
+
// ── family 1: OAuth token endpoint (RFC 6749 flat body) ─────────────────────────────────────────
|
|
25
|
+
export function oauthError(error, description, status) {
|
|
26
|
+
return { status, body: { error, error_description: description }, headers: { ...NOSTORE } };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The ONE answer an unusable authorization code gets — unknown, already-redeemed, expired, minted
|
|
30
|
+
* for another client, wrong redirect_uri pairing or failed PKCE are deliberately indistinguishable
|
|
31
|
+
* (telling them apart would leak which codes exist). The description is the widely-reported X wire
|
|
32
|
+
* string for this case; the `error` code X pairs with it is reported as `invalid_request` (where
|
|
33
|
+
* RFC 6749 would say `invalid_grant`) and the twin follows the REPORTED WIRE, marked extrapolated.
|
|
34
|
+
*/
|
|
35
|
+
export function badAuthorizationCode() {
|
|
36
|
+
return oauthError('invalid_request', 'Value passed for the authorization code was invalid.', 400);
|
|
37
|
+
}
|
|
38
|
+
/** The refresh-token twin of the above — same reported-wire caveat. */
|
|
39
|
+
export function badRefreshToken() {
|
|
40
|
+
return oauthError('invalid_request', 'Value passed for the token was invalid.', 400);
|
|
41
|
+
}
|
|
42
|
+
/** Client authentication failed at the token/revoke endpoint (RFC 6749 §5.2: 401 invalid_client). */
|
|
43
|
+
export function invalidClient() {
|
|
44
|
+
return oauthError('invalid_client', 'Client authentication failed.', 401);
|
|
45
|
+
}
|
|
46
|
+
// ── family 2: v2 problem envelopes ──────────────────────────────────────────────────────────────
|
|
47
|
+
/** The generic auth problem X answers a bad/absent/expired bearer with on /2 resources. */
|
|
48
|
+
export function unauthorizedProblem() {
|
|
49
|
+
return {
|
|
50
|
+
status: 401,
|
|
51
|
+
body: { title: 'Unauthorized', type: 'about:blank', status: 401, detail: 'Unauthorized' },
|
|
52
|
+
headers: { ...NOSTORE, 'content-type': 'application/problem+json' },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A user token whose scope set does not cover the endpoint. The 403 + problem SHAPE follows the
|
|
57
|
+
* OpenAPI's default-error contract; X's exact wording for the insufficient-scope case was not
|
|
58
|
+
* captured from an official artefact, so the twin states the refusal plainly rather than inventing
|
|
59
|
+
* vendor prose — pinning the live body is `xidentity.users_me.insufficient_scope_wording` (todo).
|
|
60
|
+
*/
|
|
61
|
+
export function forbiddenProblem(detail) {
|
|
62
|
+
return {
|
|
63
|
+
status: 403,
|
|
64
|
+
body: { title: 'Forbidden', type: 'about:blank', status: 403, detail },
|
|
65
|
+
headers: { ...NOSTORE, 'content-type': 'application/problem+json' },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** An unmodelled /2 route fails like the vendor: 404 problem, never a fake success. */
|
|
69
|
+
export function notFoundProblem() {
|
|
70
|
+
return {
|
|
71
|
+
status: 404,
|
|
72
|
+
body: { title: 'Not Found Error', type: 'about:blank', status: 404, detail: 'Not Found' },
|
|
73
|
+
headers: { ...NOSTORE, 'content-type': 'application/problem+json' },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** A contained unexpected failure on the v2 resource surface. Never exposes local exception text. */
|
|
77
|
+
export function internalServerProblem() {
|
|
78
|
+
return {
|
|
79
|
+
status: 500,
|
|
80
|
+
body: { title: 'Internal Server Error', type: 'about:blank', status: 500, detail: 'Internal Server Error' },
|
|
81
|
+
headers: { ...NOSTORE, 'content-type': 'application/problem+json' },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The invalid-parameter envelope (OpenAPI InvalidRequestProblem + the wire's per-parameter
|
|
86
|
+
* `errors` array). `parameters` maps the offending query parameter to the values it arrived with.
|
|
87
|
+
*/
|
|
88
|
+
export function invalidRequestProblem(parameters, message) {
|
|
89
|
+
return {
|
|
90
|
+
status: 400,
|
|
91
|
+
body: {
|
|
92
|
+
errors: [{ parameters, message }],
|
|
93
|
+
title: 'Invalid Request',
|
|
94
|
+
detail: 'One or more parameters to your request was invalid.',
|
|
95
|
+
type: 'https://api.x.com/2/problems/invalid-request',
|
|
96
|
+
},
|
|
97
|
+
headers: { ...NOSTORE, 'content-type': 'application/json' },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// ── family 4: the rate refusal ──────────────────────────────────────────────────────────────────
|
|
101
|
+
/** HTTP 429, legacy code 88 — the documented pair (docs.x.com fundamentals/rate-limits). */
|
|
102
|
+
export function rateLimitExceeded(headers) {
|
|
103
|
+
return {
|
|
104
|
+
status: 429,
|
|
105
|
+
body: { errors: [{ code: 88, message: 'Rate limit exceeded' }] },
|
|
106
|
+
headers: { ...NOSTORE, ...headers, 'content-type': 'application/json' },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ScopeInfo = {
|
|
2
|
+
scope: string;
|
|
3
|
+
/** X's own consent wording for the scope (OpenAPI securityScheme description). */
|
|
4
|
+
label: string;
|
|
5
|
+
/** How the authorize screen groups it: things the app can VIEW vs DO vs the session promise. */
|
|
6
|
+
group: 'view' | 'do' | 'session';
|
|
7
|
+
/** In the vendor catalog? An unknown scope is still displayed, flagged, so a typo is visible. */
|
|
8
|
+
known: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** The 24 scopes X's OpenAPI declares, plus the announcement-grounded `users.email` (see its
|
|
11
|
+
* in-line note), keyed by scope id. */
|
|
12
|
+
export declare const SCOPE_CATALOG: Record<string, {
|
|
13
|
+
label: string;
|
|
14
|
+
group: 'view' | 'do' | 'session';
|
|
15
|
+
}>;
|
|
16
|
+
export declare const KNOWN_SCOPES: readonly string[];
|
|
17
|
+
/** Space-separated scope param → ordered unique scope list (X formats scope space-separated). */
|
|
18
|
+
export declare function parseScopeParam(raw: string | null): string[];
|
|
19
|
+
export declare function formatScopeParam(scopes: readonly string[]): string;
|
|
20
|
+
export declare function describeScope(scope: string): ScopeInfo;
|
|
21
|
+
export declare function describeScopes(scopes: readonly string[]): ScopeInfo[];
|
|
22
|
+
/** Screen order: what the app can view, then what it can do, then the offline promise. */
|
|
23
|
+
export declare function sortScopesForConsent(scopes: readonly string[]): string[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// X OAuth 2.0 scopes — the vendor's own catalog, transcribed VERBATIM from the X API v2 OpenAPI
|
|
2
|
+
// document's `OAuth2UserToken` security scheme (https://api.x.com/2/openapi.json, version 2.167,
|
|
3
|
+
// fetched 2026-08-21). The description strings are X's own consent-screen wording, so the authorize
|
|
4
|
+
// page the twin serves shows exactly what the vendor's shows for each scope.
|
|
5
|
+
//
|
|
6
|
+
// The screen model: X's authorize page is ALL-OR-NOTHING. It lists what the app will be able to
|
|
7
|
+
// view and do, with exactly two buttons — "Authorize app" and "Cancel" — and no per-scope
|
|
8
|
+
// checkboxes (unlike Google's granular consent). The grant is the full requested scope set or
|
|
9
|
+
// nothing.
|
|
10
|
+
/** The 24 scopes X's OpenAPI declares, plus the announcement-grounded `users.email` (see its
|
|
11
|
+
* in-line note), keyed by scope id. */
|
|
12
|
+
export const SCOPE_CATALOG = {
|
|
13
|
+
'block.read': { label: 'View accounts you have blocked.', group: 'view' },
|
|
14
|
+
'block.write': { label: 'Block and unblock accounts on your behalf.', group: 'do' },
|
|
15
|
+
'bookmark.read': { label: 'Read your bookmarked Posts.', group: 'view' },
|
|
16
|
+
'bookmark.write': { label: 'Create and delete your bookmarks.', group: 'do' },
|
|
17
|
+
'broadcast.read': { label: 'View your live broadcasts and their chat.', group: 'view' },
|
|
18
|
+
'broadcast.write': { label: 'Manage your live broadcasts and send chat messages on your behalf.', group: 'do' },
|
|
19
|
+
'dm.read': { label: 'Read all your Direct Messages.', group: 'view' },
|
|
20
|
+
'dm.write': { label: 'Send and manage your Direct Messages.', group: 'do' },
|
|
21
|
+
'follows.read': { label: 'View accounts you follow and accounts following you.', group: 'view' },
|
|
22
|
+
'follows.write': { label: 'Follow and unfollow accounts on your behalf.', group: 'do' },
|
|
23
|
+
'like.read': { label: 'View Posts you have liked and likes you can see.', group: 'view' },
|
|
24
|
+
'like.write': { label: 'Like and unlike Posts on your behalf.', group: 'do' },
|
|
25
|
+
'list.read': { label: 'View Lists, members, and followers of Lists you created or are a member of, including private Lists.', group: 'view' },
|
|
26
|
+
'list.write': { label: 'Create and manage Lists on your behalf.', group: 'do' },
|
|
27
|
+
'media.write': { label: 'Upload media, such as photos and videos, on your behalf.', group: 'do' },
|
|
28
|
+
'mute.read': { label: 'View accounts you have muted.', group: 'view' },
|
|
29
|
+
'mute.write': { label: 'Mute and unmute accounts on your behalf.', group: 'do' },
|
|
30
|
+
'offline.access': { label: 'Stay connected to your account until you revoke access.', group: 'session' },
|
|
31
|
+
'space.read': { label: 'View all Spaces you have access to.', group: 'view' },
|
|
32
|
+
'timeline.read': { label: 'View all Custom Timelines you can see.', group: 'view' },
|
|
33
|
+
'tweet.moderate.write': { label: 'Hide and unhide replies to your posts.', group: 'do' },
|
|
34
|
+
'tweet.read': { label: 'View all posts you can see, including those from protected accounts.', group: 'view' },
|
|
35
|
+
'tweet.write': { label: 'Create and repost on your behalf.', group: 'do' },
|
|
36
|
+
'users.read': { label: 'View any account you can see, including protected accounts.', group: 'view' },
|
|
37
|
+
// NOT in the 2.167 OpenAPI securityScheme catalog (a recorded intra-vendor discrepancy): X's own
|
|
38
|
+
// April-2025 announcement ("Announcing support for email address retrieval with OAuth 2.0 in the
|
|
39
|
+
// X API v2", devcommunity.x.com/t/240555, re-fetched 2026-08-21) adds `users.email` as the scope
|
|
40
|
+
// gating the `confirmed_email` field on /2/users/me. The label is NOT vendor wording —
|
|
41
|
+
// EXTRAPOLATION, pinned by `xidentity.scopes.users_email_wording` (todo).
|
|
42
|
+
'users.email': { label: 'Access your email address.', group: 'view' },
|
|
43
|
+
};
|
|
44
|
+
// EVIDENCE BOUNDARY: every label above is the OpenAPI description verbatim EXCEPT `offline.access`,
|
|
45
|
+
// whose OpenAPI description ("Request a refresh token for the app.") is developer-facing, not the
|
|
46
|
+
// consent-screen line. The consent-screen wording used here is the widely-reported "Stay connected
|
|
47
|
+
// …" line; it is not from a fetched official artefact and is marked as such in the manifest
|
|
48
|
+
// (`xidentity.consent.offline_access_wording`, todo).
|
|
49
|
+
export const KNOWN_SCOPES = Object.keys(SCOPE_CATALOG);
|
|
50
|
+
/** Space-separated scope param → ordered unique scope list (X formats scope space-separated). */
|
|
51
|
+
export function parseScopeParam(raw) {
|
|
52
|
+
if (!raw)
|
|
53
|
+
return [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const s of raw.split(/[\s+]+/)) {
|
|
57
|
+
// `+` appears when a caller form-encodes spaces as plus signs and a layer decodes only %XX.
|
|
58
|
+
if (!s || seen.has(s))
|
|
59
|
+
continue;
|
|
60
|
+
seen.add(s);
|
|
61
|
+
out.push(s);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export function formatScopeParam(scopes) {
|
|
66
|
+
return scopes.join(' ');
|
|
67
|
+
}
|
|
68
|
+
export function describeScope(scope) {
|
|
69
|
+
const known = SCOPE_CATALOG[scope];
|
|
70
|
+
return known
|
|
71
|
+
? { scope, label: known.label, group: known.group, known: true }
|
|
72
|
+
: { scope, label: scope, group: 'view', known: false };
|
|
73
|
+
}
|
|
74
|
+
export function describeScopes(scopes) {
|
|
75
|
+
return scopes.map(describeScope);
|
|
76
|
+
}
|
|
77
|
+
/** Screen order: what the app can view, then what it can do, then the offline promise. */
|
|
78
|
+
export function sortScopesForConsent(scopes) {
|
|
79
|
+
const order = { view: 0, do: 1, session: 2 };
|
|
80
|
+
return [...scopes].sort((a, b) => order[describeScope(a).group] - order[describeScope(b).group]);
|
|
81
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Options every X-identity-twin HTTP surface needs, independent of who owns the socket. */
|
|
2
|
+
export interface XIdentityTwinFetchOptions {
|
|
3
|
+
root?: string;
|
|
4
|
+
readOnly?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The pack's whole HTTP surface as a plain `fetch` — Request in, Response out, no listener.
|
|
8
|
+
*
|
|
9
|
+
* This is the composable form (runtime contract R12b): a Worker / Durable Object entry has NO
|
|
10
|
+
* loopback ports, so it must mount a pack's handler IN-PROCESS. `createXIdentityTwinServer` is
|
|
11
|
+
* nothing but `Bun.serve` wrapped around this closure, so the standalone (R1) and hosted
|
|
12
|
+
* surfaces are the SAME code — there is no second HTTP adaptation to drift.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IT SERVES IS UNCHANGED (R9): GET /twin is built from constants, and the only clock on
|
|
15
|
+
* the path is `worldNow()`, the world's frozen instant. The authorize screen's own script and
|
|
16
|
+
* stylesheet routes serve COMMITTED text (`xidentity-consent-client.gen.ts`, built on the dev
|
|
17
|
+
* plane by `scripts/consent-clients.ts`) — no build and no disk read on the fetch path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createXIdentityTwinFetch(options?: XIdentityTwinFetchOptions): (request: Request) => Promise<Response>;
|
|
20
|
+
export declare function createXIdentityTwinServer(options: {
|
|
21
|
+
root?: string;
|
|
22
|
+
port?: number;
|
|
23
|
+
readOnly?: boolean;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
port: number;
|
|
26
|
+
stop: () => void;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* The authorize screen is served BY THE TWIN, at the vendor's own path — there is no second
|
|
30
|
+
* "mirror" server to start. This alias exists so the `world-xidentity mirror` command and the
|
|
31
|
+
* journey harness have the conventional entry point, and it returns the very same server.
|
|
32
|
+
*/
|
|
33
|
+
export declare const createXIdentityConsentServer: typeof createXIdentityTwinServer;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// X identity twin HTTP server — one server for the WHOLE vendor surface, because sign-in-with-X is
|
|
2
|
+
// one product spread over two host families. Point `x.com`, `twitter.com`, `api.x.com` and
|
|
3
|
+
// `api.twitter.com` here (the injector's `xidentity` VENDOR_HOSTS entry does exactly that) and an
|
|
4
|
+
// unmodified X client completes a full authorization-code round trip against it.
|
|
5
|
+
//
|
|
6
|
+
// Two things this server does that a JSON-only twin server does not:
|
|
7
|
+
// • it can answer with HTML and with 302 redirects — the authorize screen and the callback
|
|
8
|
+
// bounce are the protocol, not decoration, so the handler's `headers` (content-type, location)
|
|
9
|
+
// pass straight through rather than being flattened into a JSON envelope;
|
|
10
|
+
// • it serves the authorize page's own assets under the twin-namespaced `/_twin/assets/*`.
|
|
11
|
+
//
|
|
12
|
+
// It also tells the handler its OWN origin, so the consent form's action points BACK AT THE TWIN.
|
|
13
|
+
import { serveHttp } from '@volter/world-core';
|
|
14
|
+
import { CONSENT_CLIENT_CSS, CONSENT_CLIENT_JS, CONSENT_SCRIPT_PATH, CONSENT_STYLE_PATH } from "./xidentity-consent-ui.js";
|
|
15
|
+
import { worldNow, statefulTwinManifest, twinPublicBase } from '@volter/world-core';
|
|
16
|
+
import { handleXIdentityTwinRequest } from "./xidentity-twin.js";
|
|
17
|
+
/**
|
|
18
|
+
* The pack's whole HTTP surface as a plain `fetch` — Request in, Response out, no listener.
|
|
19
|
+
*
|
|
20
|
+
* This is the composable form (runtime contract R12b): a Worker / Durable Object entry has NO
|
|
21
|
+
* loopback ports, so it must mount a pack's handler IN-PROCESS. `createXIdentityTwinServer` is
|
|
22
|
+
* nothing but `Bun.serve` wrapped around this closure, so the standalone (R1) and hosted
|
|
23
|
+
* surfaces are the SAME code — there is no second HTTP adaptation to drift.
|
|
24
|
+
*
|
|
25
|
+
* WHAT IT SERVES IS UNCHANGED (R9): GET /twin is built from constants, and the only clock on
|
|
26
|
+
* the path is `worldNow()`, the world's frozen instant. The authorize screen's own script and
|
|
27
|
+
* stylesheet routes serve COMMITTED text (`xidentity-consent-client.gen.ts`, built on the dev
|
|
28
|
+
* plane by `scripts/consent-clients.ts`) — no build and no disk read on the fetch path.
|
|
29
|
+
*/
|
|
30
|
+
export function createXIdentityTwinFetch(options = {}) {
|
|
31
|
+
const readOnly = options.readOnly ?? false;
|
|
32
|
+
return async function xIdentityTwinFetch(request) {
|
|
33
|
+
const url = new URL(request.url);
|
|
34
|
+
// GET /twin — the discovery manifest (education inside the twin).
|
|
35
|
+
if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin') {
|
|
36
|
+
return Response.json(statefulTwinManifest({ vendor: 'xidentity', twinOf: 'X (Twitter) identity / OAuth 2.0 + PKCE', stores: 'authorization codes, access/refresh tokens and the authorize-screen round trip' }));
|
|
37
|
+
}
|
|
38
|
+
if (request.method === 'GET' && url.pathname === CONSENT_SCRIPT_PATH) {
|
|
39
|
+
return new Response(CONSENT_CLIENT_JS, { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
40
|
+
}
|
|
41
|
+
if (request.method === 'GET' && url.pathname === CONSENT_STYLE_PATH) {
|
|
42
|
+
return new Response(CONSENT_CLIENT_CSS, { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
43
|
+
}
|
|
44
|
+
const body = request.method === 'GET' || request.method === 'HEAD' ? '' : await request.text();
|
|
45
|
+
const headers = {};
|
|
46
|
+
request.headers.forEach((value, key) => {
|
|
47
|
+
headers[key.toLowerCase()] = value;
|
|
48
|
+
});
|
|
49
|
+
const res = await handleXIdentityTwinRequest({
|
|
50
|
+
method: request.method,
|
|
51
|
+
path: url.pathname + (url.search || ''),
|
|
52
|
+
body,
|
|
53
|
+
headers,
|
|
54
|
+
readOnly,
|
|
55
|
+
occurredAt: worldNow(),
|
|
56
|
+
origin: twinPublicBase(request),
|
|
57
|
+
callbackOrigin: url.origin,
|
|
58
|
+
...(options.root !== undefined ? { root: options.root } : {}),
|
|
59
|
+
});
|
|
60
|
+
const out = { ...(res.headers ?? {}) };
|
|
61
|
+
// A string body is already rendered (HTML, or the empty body of a 302); anything else is the
|
|
62
|
+
// vendor's JSON.
|
|
63
|
+
if (typeof res.body === 'string') {
|
|
64
|
+
if (!out['content-type'] && res.body)
|
|
65
|
+
out['content-type'] = 'text/html; charset=utf-8';
|
|
66
|
+
return new Response(res.body, { status: res.status, headers: out });
|
|
67
|
+
}
|
|
68
|
+
out['content-type'] = out['content-type'] ?? 'application/json; charset=utf-8';
|
|
69
|
+
return new Response(JSON.stringify(res.body), { status: res.status, headers: out });
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export async function createXIdentityTwinServer(options) {
|
|
73
|
+
const server = await serveHttp({
|
|
74
|
+
port: options.port ?? 0,
|
|
75
|
+
idleTimeout: 60,
|
|
76
|
+
fetch: createXIdentityTwinFetch(options),
|
|
77
|
+
});
|
|
78
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The authorize screen is served BY THE TWIN, at the vendor's own path — there is no second
|
|
82
|
+
* "mirror" server to start. This alias exists so the `world-xidentity mirror` command and the
|
|
83
|
+
* journey harness have the conventional entry point, and it returns the very same server.
|
|
84
|
+
*/
|
|
85
|
+
export const createXIdentityConsentServer = createXIdentityTwinServer;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type TwinResource } from '@volter/world-core';
|
|
2
|
+
export declare const SERVICE = "xidentity";
|
|
3
|
+
export declare const RESOURCE_TYPES: readonly ["oauth_client", "account", "session", "auth_request", "authorization_code", "access_token", "refresh_token", "grant", "rate_window"];
|
|
4
|
+
export type ResourceType = (typeof RESOURCE_TYPES)[number];
|
|
5
|
+
export type Row = TwinResource & Record<string, any>;
|
|
6
|
+
export declare function readAll(root: string | undefined): Row[];
|
|
7
|
+
export declare function readType(root: string | undefined, type: ResourceType): Row[];
|
|
8
|
+
export declare function readOne(root: string | undefined, type: ResourceType, id: string): Row | undefined;
|
|
9
|
+
/** The next `rev` for a subject that is being rewritten in place (survives deletes/tombstones). */
|
|
10
|
+
export declare function nextRev(root: string | undefined, type: ResourceType, id: string): number;
|
|
11
|
+
/** The next revision from a snapshot already held under the kernel projection lock. */
|
|
12
|
+
export declare function nextRevIn(resources: readonly TwinResource[], type: ResourceType, id: string): number;
|
|
13
|
+
/** A state-dependent one-row update whose read/revision/write decision is one kernel transaction. */
|
|
14
|
+
export declare function writeAtomic(type: ResourceType, id: string, operation: string, fields: (resources: readonly TwinResource[]) => Record<string, unknown>, opts: WriteOpts): Promise<Row>;
|
|
15
|
+
export type WriteOpts = {
|
|
16
|
+
root?: string;
|
|
17
|
+
occurredAt?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Seeding also needs the origin the twin was reached on, so the demo clients' registered
|
|
20
|
+
* callbacks point back at THIS twin rather than at a baked-in port (runtime contract R7). */
|
|
21
|
+
export type SeedOpts = WriteOpts & {
|
|
22
|
+
origin?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function write(type: ResourceType, id: string, operation: string, fields: Record<string, unknown>, opts: WriteOpts): Promise<Row>;
|
|
25
|
+
/** An X OAuth 2.0 client id: base64url of `<opaque>:1:ci`. */
|
|
26
|
+
export declare const mintClientId: (root: string | undefined, instant: string) => string;
|
|
27
|
+
/** An X OAuth 2.0 authorization code (`…:ac:1`). `subject` is the authorize screen it settles. */
|
|
28
|
+
export declare const mintAuthorizationCode: (root: string | undefined, at: number, subject: string) => string;
|
|
29
|
+
/** An X OAuth 2.0 user access token (`…:at:1`). `subject` is the credential exchanged for it. */
|
|
30
|
+
export declare const mintAccessToken: (root: string | undefined, at: number, subject: string) => string;
|
|
31
|
+
/** An X OAuth 2.0 refresh token (`…:rt:1`). `subject` is the credential exchanged for it. */
|
|
32
|
+
export declare const mintRefreshToken: (root: string | undefined, at: number, subject: string) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Twin-internal handle for an authorize screen in flight (never leaves the twin's own pages) —
|
|
35
|
+
* `ar_` + 32 hex, the shape `randomUUID()` gave it, now a pure function of (request, stored state).
|
|
36
|
+
*
|
|
37
|
+
* An authorize request whose parameters ALREADY name an unsettled pending row reuses that row's
|
|
38
|
+
* id: pressing reload on the authorize screen is the same screen, not a new one. Otherwise the id
|
|
39
|
+
* is `stableHex(auth_request:<world instant>:<rows already held>)` — two authorize requests at one
|
|
40
|
+
* instant differ by the count, and two identical worlds mint identical ids.
|
|
41
|
+
*/
|
|
42
|
+
export declare function authRequestIdFor(root: string | undefined, occurredAt: string, fields: Record<string, unknown>): string;
|
|
43
|
+
export declare const DEFAULT_CLIENT_ID = "VHdpbkRlbW9BcHAwMDAwMDAwMDA6MTpjaQ";
|
|
44
|
+
export declare const DEFAULT_CLIENT_SECRET = "twin-confidential-secret-0000000000000000000000000";
|
|
45
|
+
export declare const DEFAULT_PUBLIC_CLIENT_ID = "VHdpblB1YmxpY0FwcDAwMDAwMDA6MTpjaQ";
|
|
46
|
+
export type SeedAccount = {
|
|
47
|
+
id: string;
|
|
48
|
+
username: string;
|
|
49
|
+
name: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
description: string;
|
|
52
|
+
location?: string;
|
|
53
|
+
profileImageUrl?: string;
|
|
54
|
+
protectedAccount: boolean;
|
|
55
|
+
verified: boolean;
|
|
56
|
+
verifiedType: 'none' | 'blue' | 'business' | 'government';
|
|
57
|
+
publicMetrics: {
|
|
58
|
+
followers_count: number;
|
|
59
|
+
following_count: number;
|
|
60
|
+
tweet_count: number;
|
|
61
|
+
listed_count: number;
|
|
62
|
+
like_count: number;
|
|
63
|
+
media_count: number;
|
|
64
|
+
};
|
|
65
|
+
url?: string;
|
|
66
|
+
confirmedEmail?: string;
|
|
67
|
+
};
|
|
68
|
+
/** X user ids are stable numeric strings (the docs' own example user is "2244994945"). The seeded
|
|
69
|
+
* ids start at 9e18 — vendor-SHAPED (int64-range numeric) but far ABOVE the live snowflake id
|
|
70
|
+
* space (~1.9e18 in 2026), so a locally seeded persona can never collide with a pulled real id
|
|
71
|
+
* (the datadog EVENT_ID_BASE precedent; §9 round one). */
|
|
72
|
+
export declare const DEFAULT_ACCOUNTS: SeedAccount[];
|
|
73
|
+
/**
|
|
74
|
+
* The redirect URIs the seeded demo clients register.
|
|
75
|
+
*
|
|
76
|
+
* Runtime contract R7: a twin NEVER bakes a port into served content or config. The callbacks are
|
|
77
|
+
* DERIVED from the origin the twin was actually reached on (`XIdentityRequest.origin`, which
|
|
78
|
+
* xidentity-server fills from the URL the request arrived at). A caller that declares no origin —
|
|
79
|
+
* an in-process call — seeds no callbacks at all and registers its own through the twin door.
|
|
80
|
+
*/
|
|
81
|
+
export declare function defaultRedirectUris(origin?: string): string[];
|
|
82
|
+
/**
|
|
83
|
+
* Materialise the default clients + accounts + session once per root. Idempotent by subject id:
|
|
84
|
+
* re-running it over a seeded root writes nothing new, and it NEVER overwrites an
|
|
85
|
+
* operator-registered client, account or session choice.
|
|
86
|
+
*/
|
|
87
|
+
export declare function ensureSeed(opts: SeedOpts): Promise<void>;
|
|
88
|
+
/** The account the x.com browser session is signed in as (the one the authorize screen consents). */
|
|
89
|
+
export declare function sessionAccount(root: string | undefined): Row | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Is `candidate` an EXACT registered callback URI for this client? X documents exact-match
|
|
92
|
+
* validation for OAuth 2.0 callback URLs ("This value must correspond to one of the Callback URLs
|
|
93
|
+
* defined in your App's settings" — and the legacy official SDK's own docblock spells out "exact
|
|
94
|
+
* match validation"). No loopback-port exception is documented for X, so none is modelled — a
|
|
95
|
+
* loosely-matching twin would hide the single most common integration bug.
|
|
96
|
+
*/
|
|
97
|
+
export declare function redirectUriAllowed(client: Row, candidate: string): boolean;
|