@volter/twin-xidentity 0.1.0 → 0.1.2

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 CHANGED
@@ -41,10 +41,10 @@ with X's consent descriptions; the **docs.x.com OAuth 2.0 guides** for the autho
41
41
  revoke requests; the **docs.x.com rate-limit pages** for the `x-rate-limit-*` headers and the
42
42
  75/15-minute per-user figure; and the **official SDK sources** (`@xdevplatform/xdk`,
43
43
  `twitter-api-typescript-sdk`) for the token/revoke response shapes the docs never show as JSON.
44
- It currently reads **82 done / 131 covered** (49 `todo`, and the login leg is not an API surface: it is the seeded session, so it is not in that
44
+ It currently reads **98 done / 160 covered** (62 `todo`, and the login leg is not an API surface: it is the seeded session, so it is not in that
45
45
  denominator). The `todo`s are real X identity
46
- surface this twin does not model — most notably **OAuth 1.0a three-legged sign-in**, the **v1.1
47
- app-only bearer flow**, the unmodelled `user.fields` (entities, withheld, subscription, the
46
+ surface this twin does not model — most notably the **v1.1 app-only bearer flow**, OAuth 1.0a's
47
+ timestamp window and nonce replay, the unmodelled `user.fields` (entities, withheld, subscription, the
48
48
  relational fields), expansion hydration — plus a family of **wire-pinning todos**: behaviours the
49
49
  twin models from RFC 6749/7009 or widely-reported captures because no fetched official artefact
50
50
  states them (exact token error wording, the authorize error channel, refresh-token rotation, the
@@ -74,6 +74,37 @@ code TTL). Each such `done` names its evidence boundary in the manifest and its
74
74
  (`xidentity-sdk.integration.test.ts`); the OAuth legs its hardcoded hosts cannot re-aim are
75
75
  proven over real transport in the docs' own curl shapes.
76
76
 
77
+ ### OAuth 1.0a (`src/xidentity-oauth1.ts`)
78
+
79
+ The three-legged flow as docs.x.com's API reference and "Obtaining access tokens using 3-legged
80
+ OAuth flow" state it, which is what Postiz (`gitroomhq/postiz-app`) connects an X account with
81
+ through `twitter-api-v2`:
82
+
83
+ - `POST oauth/request_token`, signed by a registered App with HMAC-SHA1, answers `oauth_token`,
84
+ `oauth_token_secret` and `oauth_callback_confirmed=true`; a callback the App does not list is
85
+ code 415, `oob` is the PIN flow, and `x_auth_access_type=read` narrows a Read-and-write App.
86
+ - `GET oauth/authorize` and `GET oauth/authenticate` (served on `api.x.com`, where X serves them)
87
+ show the screen naming the App and the signed-in account, listing what its permission allows;
88
+ its form posts back to `POST oauth/authorize` on the same host, as X's screen does (on api.x.com,
89
+ which the `x` pack shares, that path and the screen's `/_twin/assets/consent.{js,css}` are this
90
+ pack's). Authorize app returns to the callback with exactly `oauth_token` + `oauth_verifier`, Cancel with
91
+ `denied=<request token>`. `authenticate` skips the screen once the person approved the App
92
+ (unless `force_login=true`).
93
+ - `POST oauth/access_token` trades the approved request token and verifier, once, for
94
+ `<user id>-<token>`, its secret, `user_id` and `screen_name`; approving the same App again returns
95
+ the token already held. `POST 1.1/oauth/invalidate_token` revokes it.
96
+ - The access token signs `GET /2/users/me` here and the posting surface in the `x` pack (which
97
+ reads this pack's `oauth1_token` rows). An unknown consumer key or a signature that does not
98
+ verify is code 32 at the legs and the `about:blank` 401 on `/2`.
99
+ - The **World's own App** is the pair the World's env names (`X_API_KEY` / `X_API_SECRET`, or
100
+ `TWITTER_API_KEY` / `TWITTER_API_SECRET`), read with `worldEnvValue`, named for the World and
101
+ accepting any callback — the slack pack's World-app rule. Other Apps are registered through
102
+ `POST /_twin/oauth1_apps`.
103
+
104
+ Not modelled (manifest todos): the timestamp window and nonce replay (the twin's clock is the
105
+ World's, an app signs with its host's), request-token expiry, the login step `force_login` and
106
+ `screen_name` would show, and the live wording of several refusals, each pinned by name.
107
+
77
108
  ### What is not
78
109
 
79
110
  The twin **authenticates nobody**: no password, no 2FA, no risk engine. The
@@ -86,7 +117,9 @@ service-areas to add within this same pack; they are not a reason to create a se
86
117
 
87
118
  `GET /_twin/consent` (re-render a pending authorize screen by request handle) and
88
119
  `POST /_twin/consent` (the Authorize/Cancel form post — X's real form posts to an undocumented
89
- internal endpoint), `POST /_twin/clients`, `POST /_twin/accounts`, `POST /_twin/session`,
120
+ internal endpoint; the OAuth 1.0a screen's posts to `POST oauth/authorize`, above),
121
+ `POST /_twin/oauth1_apps` (register an OAuth 1.0a App: consumer key and secret, callback URLs,
122
+ permission), `POST /_twin/clients`, `POST /_twin/accounts`, `POST /_twin/session`,
90
123
  `POST /_twin/rate_limit` (arm a deterministic 429), and the `/_twin/assets/*` page assets. This
91
124
  list is the audit trail the conformance census deliberately excludes — keep it in lockstep with
92
125
  `twinControl`'s branches in `xidentity-twin.ts`.
@@ -44,8 +44,12 @@ export type ConsentView = {
44
44
  /** The x.com session this browser is signed in as — the account that will consent. */
45
45
  account: ConsentAccount;
46
46
  scopes: ConsentScopeRow[];
47
- /** Host of the validated redirect_uri, shown in the "you'll be redirected to" notice. */
47
+ /** Host of the validated redirect_uri, shown in the "you'll be redirected to" notice. Empty for
48
+ * an OAuth 1.0a out-of-band (PIN) request, which redirects nowhere. */
48
49
  redirectHost: string;
50
+ /** Where the decision posts, under `origin`: `/_twin/consent` for an OAuth 2.0 authorize screen,
51
+ * `/oauth/authorize` for an OAuth 1.0a one, as X's own screen posts (the request token is the handle). */
52
+ decisionPath?: string;
49
53
  };
50
54
 
51
55
  /** The X mark, drawn rather than fetched — a twin never reaches out to a vendor CDN. */
@@ -96,7 +100,7 @@ export function ConsentPage({ view }: { view: ConsentView }) {
96
100
  <ScopeSection heading={`Things ${appName} can view`} rows={view.scopes.filter((s) => s.group === 'view')} />
97
101
  <ScopeSection heading={`Things ${appName} can do`} rows={view.scopes.filter((s) => s.group === 'do')} />
98
102
  <ScopeSection heading="Until you revoke access" rows={view.scopes.filter((s) => s.group === 'session')} />
99
- <form method="POST" action={`${view.origin}/_twin/consent`} className="consent-form">
103
+ <form method="POST" action={`${view.origin}${view.decisionPath ?? '/_twin/consent'}`} className="consent-form">
100
104
  <input type="hidden" name="auth_request" value={view.requestId} />
101
105
  <div className="actions">
102
106
  <button className="btn btn-primary" type="submit" name="decision" value="allow">
@@ -108,8 +112,41 @@ export function ConsentPage({ view }: { view: ConsentView }) {
108
112
  </div>
109
113
  </form>
110
114
  <p className="legal">
111
- You&apos;ll be redirected to <strong className="redirect-host">{view.redirectHost}</strong>. You can revoke
112
- access to any app at any time from the Apps and sessions section of your X settings.
115
+ {view.redirectHost ? (
116
+ <>
117
+ You&apos;ll be redirected to <strong className="redirect-host">{view.redirectHost}</strong>.{' '}
118
+ </>
119
+ ) : null}
120
+ You can revoke access to any app at any time from the Apps and sessions section of your X settings.
121
+ </p>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export type PinPageProps = {
127
+ appName: string;
128
+ /** The oauth_verifier, shown as the PIN the person types into the app (out-of-band flow). */
129
+ pin: string;
130
+ };
131
+
132
+ /**
133
+ * The OAuth 1.0a out-of-band ending: an app that registered `oauth_callback=oob` gets no redirect,
134
+ * so X shows the verifier as a PIN for the person to type into the app (docs.x.com PIN-based
135
+ * authorization). The layout follows the classic screen as widely screenshotted; its live DOM was
136
+ * not captured (`xidentity.oauth1.consent_wording`, todo).
137
+ */
138
+ export function PinPage({ appName, pin }: PinPageProps) {
139
+ return (
140
+ <div className="card">
141
+ <header className="x-header">
142
+ <XMark />
143
+ </header>
144
+ <h1 className="title">
145
+ You&apos;ve granted access to <strong className="app-name">{appName}</strong>!
146
+ </h1>
147
+ <p className="legal">Next, return to {appName} and enter this PIN to complete the authorization process:</p>
148
+ <p className="title">
149
+ <code className="oauth-pin">{pin}</code>
113
150
  </p>
114
151
  </div>
115
152
  );