@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 +37 -4
- package/client/xidentity-consent.tsx +41 -4
- package/dist/client/xidentity-consent.bundle.js +85 -85
- package/dist/client/xidentity-consent.d.ts +17 -1
- package/dist/client/xidentity-consent.js +11 -2
- package/dist/client/xidentity-consent.tsx +41 -4
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +16 -8
- package/dist/src/xidentity-capabilities.js +243 -8
- package/dist/src/xidentity-conformance.d.ts +41 -0
- package/dist/src/xidentity-conformance.js +210 -3
- package/dist/src/xidentity-consent-client.gen.d.ts +1 -1
- package/dist/src/xidentity-consent-client.gen.js +1 -1
- package/dist/src/xidentity-consent-ui.d.ts +25 -1
- package/dist/src/xidentity-consent-ui.js +62 -1
- package/dist/src/xidentity-oauth1.d.ts +93 -0
- package/dist/src/xidentity-oauth1.js +594 -0
- package/dist/src/xidentity-server.js +14 -4
- package/dist/src/xidentity-store.d.ts +4 -1
- package/dist/src/xidentity-store.js +8 -1
- package/dist/src/xidentity-twin.js +32 -9
- package/package.json +5 -4
- package/src/index.ts +30 -8
- package/src/xidentity-capabilities.ts +254 -8
- package/src/xidentity-conformance.ts +232 -3
- package/src/xidentity-consent-client.gen.ts +1 -1
- package/src/xidentity-consent-ui.ts +67 -0
- package/src/xidentity-oauth1.ts +598 -0
- package/src/xidentity-server.ts +14 -4
- package/src/xidentity-store.ts +8 -1
- package/src/xidentity-twin.ts +30 -9
package/src/xidentity-store.ts
CHANGED
|
@@ -41,6 +41,13 @@ export const RESOURCE_TYPES = [
|
|
|
41
41
|
'refresh_token',
|
|
42
42
|
'grant',
|
|
43
43
|
'rate_window',
|
|
44
|
+
// OAuth 1.0a (xidentity-oauth1.ts): an App registered through the twin door, a request token
|
|
45
|
+
// in flight between the three legs, an issued access token, and the (app, user) grant that lets
|
|
46
|
+
// GET oauth/authenticate skip a screen already approved.
|
|
47
|
+
'oauth1_app',
|
|
48
|
+
'oauth1_request_token',
|
|
49
|
+
'oauth1_token',
|
|
50
|
+
'oauth1_grant',
|
|
44
51
|
] as const;
|
|
45
52
|
export type ResourceType = (typeof RESOURCE_TYPES)[number];
|
|
46
53
|
|
|
@@ -174,7 +181,7 @@ export const mintRefreshToken = (root: string | undefined, at: number, subject:
|
|
|
174
181
|
// ── the authorize screen's handle: DETERMINISTIC, because it is SERVED (R9) ─────────────────────
|
|
175
182
|
/** FNV-1a over the seed, widened by re-hashing with a round counter until `n` hex chars exist
|
|
176
183
|
* (the resend exemplar's `stableHex`). Pure: no clock, no entropy, no host state. */
|
|
177
|
-
function stableHex(seed: string, n: number): string {
|
|
184
|
+
export function stableHex(seed: string, n: number): string {
|
|
178
185
|
let out = '';
|
|
179
186
|
for (let round = 0; out.length < n; round += 1) {
|
|
180
187
|
let h = 0x811c9dc5;
|
package/src/xidentity-twin.ts
CHANGED
|
@@ -59,6 +59,7 @@ import { applyTwinWriteAtomic, type TwinResource } from '@volter/world-core';
|
|
|
59
59
|
import { consentPageHtml, errorPageHtml, xIdentityConsentState } from './xidentity-consent-ui.ts';
|
|
60
60
|
import { formatScopeParam, parseScopeParam } from './xidentity-scopes.ts';
|
|
61
61
|
import { normalizeChallengeMethod, pkceVerifies } from './xidentity-pkce.ts';
|
|
62
|
+
import { OAUTH1_ENDPOINTS, OAUTH1_WRITE_PATHS, routeOAuth1, scopesOfAccessLevel, verifyUserContext } from './xidentity-oauth1.ts';
|
|
62
63
|
import {
|
|
63
64
|
authRequestIdFor,
|
|
64
65
|
ensureSeed,
|
|
@@ -688,8 +689,11 @@ function enumMessage(param: string, bad: string[], allowed: readonly string[]):
|
|
|
688
689
|
async function usersMe(req: XIdentityRequest, query: URLSearchParams): Promise<XResponse> {
|
|
689
690
|
const root = req.root;
|
|
690
691
|
const header = req.headers?.authorization ?? '';
|
|
691
|
-
|
|
692
|
-
|
|
692
|
+
// Two schemes, as the OpenAPI's security for this operation lists them: an OAuth 2.0 user bearer
|
|
693
|
+
// (OAuth2UserToken) or OAuth 1.0a User Context (UserToken), verified in xidentity-oauth1.ts.
|
|
694
|
+
const oauth1 = /^oauth\s/i.test(header);
|
|
695
|
+
const bearer = oauth1 ? undefined : /^bearer\s+(.+)$/i.exec(header)?.[1]?.trim();
|
|
696
|
+
if (!bearer && !oauth1) return unauthorizedProblem();
|
|
693
697
|
|
|
694
698
|
// Parameter validation happens BEFORE rate accounting: a 400 is not a spent request.
|
|
695
699
|
// EXTRAPOLATION on the ordering (`xidentity.rate.what_counts`, todo).
|
|
@@ -708,17 +712,29 @@ async function usersMe(req: XIdentityRequest, query: URLSearchParams): Promise<X
|
|
|
708
712
|
// `xidentity.users_me.expansion_hydration` (todo) owns the honest gap.
|
|
709
713
|
|
|
710
714
|
const evaluate = (resources: readonly TwinResource[], spend: boolean) => {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
+
let sub: string;
|
|
716
|
+
let held: string[];
|
|
717
|
+
if (oauth1) {
|
|
718
|
+
// A signed request with an access token this pack issued and has not revoked; anything else
|
|
719
|
+
// (unknown consumer, bad signature, revoked token) is the same about:blank 401 a bad bearer gets.
|
|
720
|
+
const who = verifyUserContext(req, resources);
|
|
721
|
+
if (!who) return { response: unauthorizedProblem() };
|
|
722
|
+
sub = String(who.token.userId);
|
|
723
|
+
held = scopesOfAccessLevel(who.token.accessLevel);
|
|
724
|
+
} else {
|
|
725
|
+
const tokenRow = resources.find((row) => row.type === 'access_token' && row.id === bearer) as Row | undefined;
|
|
726
|
+
if (!tokenRow || tokenRow.revoked === true
|
|
727
|
+
|| (typeof tokenRow.expiresAt === 'number' && nowSeconds(req.occurredAt) >= tokenRow.expiresAt)) {
|
|
728
|
+
return { response: unauthorizedProblem() };
|
|
729
|
+
}
|
|
730
|
+
sub = String(tokenRow.sub);
|
|
731
|
+
held = parseScopeParam(String(tokenRow.scope));
|
|
715
732
|
}
|
|
716
|
-
const held = parseScopeParam(String(tokenRow.scope));
|
|
717
733
|
const missing = USERS_ME_REQUIRED_SCOPES.filter((scope) => !held.includes(scope));
|
|
718
734
|
if (missing.length > 0) {
|
|
719
735
|
return { response: forbiddenProblem(`The access token does not carry the scope(s) required by this endpoint: ${missing.join(', ')}.`) };
|
|
720
736
|
}
|
|
721
|
-
const account = resources.find((row) => row.type === 'account' && row.id ===
|
|
737
|
+
const account = resources.find((row) => row.type === 'account' && row.id === sub) as Row | undefined;
|
|
722
738
|
if (!account) return { response: unauthorizedProblem() };
|
|
723
739
|
const now = nowSeconds(req.occurredAt);
|
|
724
740
|
const windowId = `users_me:${account.id}`;
|
|
@@ -779,6 +795,7 @@ export function xIdentityTwinSnapshot(): { implementedEndpoints: string[]; resou
|
|
|
779
795
|
'POST /2/oauth2/token',
|
|
780
796
|
'POST /2/oauth2/revoke',
|
|
781
797
|
'GET /2/users/me',
|
|
798
|
+
...OAUTH1_ENDPOINTS,
|
|
782
799
|
],
|
|
783
800
|
resourceTypes: STORE_RESOURCE_TYPES,
|
|
784
801
|
grantTypes: ['authorization_code', 'refresh_token'],
|
|
@@ -921,7 +938,8 @@ async function routeXIdentityTwinRequest(req: XIdentityRequest): Promise<XRespon
|
|
|
921
938
|
// D3: a read-only twin cannot mint credentials or settle consent. GET /2/users/me stays served
|
|
922
939
|
// (a read), with rate accounting suspended. The refusal is an OAuth error body, so a client sees
|
|
923
940
|
// a protocol error rather than an HTML surprise.
|
|
924
|
-
const writes = path.startsWith('/_twin/') || path === '/2/oauth2/token' || path === '/2/oauth2/revoke'
|
|
941
|
+
const writes = path.startsWith('/_twin/') || path === '/2/oauth2/token' || path === '/2/oauth2/revoke' || OAUTH1_WRITE_PATHS.has(path)
|
|
942
|
+
|| (method === 'POST' && path === '/oauth/authorize');
|
|
925
943
|
if (req.readOnly && writes) {
|
|
926
944
|
return oauthError('temporarily_unavailable', 'this twin was started read-only; omit readOnly to accept writes', 405);
|
|
927
945
|
}
|
|
@@ -930,6 +948,9 @@ async function routeXIdentityTwinRequest(req: XIdentityRequest): Promise<XRespon
|
|
|
930
948
|
// a world with clients, personas and a signed-in session — exactly what a browser at x.com has.
|
|
931
949
|
if (!req.readOnly) await ensureSeed({ root: req.root, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), ...((req.callbackOrigin ?? req.origin) ? { origin: req.callbackOrigin ?? req.origin } : {}) });
|
|
932
950
|
|
|
951
|
+
const oauth1 = await routeOAuth1(req, method, path);
|
|
952
|
+
if (oauth1) return oauth1;
|
|
953
|
+
|
|
933
954
|
const control = await twinControl(req, path, form);
|
|
934
955
|
if (control) return control;
|
|
935
956
|
|