@volter/twin-xidentity 0.1.2 → 0.1.4
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
|
@@ -68,7 +68,10 @@ code TTL). Each such `done` names its evidence boundary in the manifest and its
|
|
|
68
68
|
- **The identity read** — `GET /2/users/me` with the OpenAPI's scope demands (`tweet.read` +
|
|
69
69
|
`users.read`), the 24-value `user.fields` enum with the wire's invalid-parameter envelope, the
|
|
70
70
|
documented `x-rate-limit-*` headers, 75/15min per-user accounting, and the documented
|
|
71
|
-
429 + legacy code 88 refusal.
|
|
71
|
+
429 + legacy code 88 refusal. `user.fields=profile_image_url` is the seeded photo, or X's default
|
|
72
|
+
avatar URL (`abs.twimg.com/…/default_profile_normal.png`) for a persona seeded without one — the
|
|
73
|
+
same answer the `x` pack gives for the same person; a pulled persona whose reply withheld the field
|
|
74
|
+
is not given one.
|
|
72
75
|
- **SDK fidelity** — the unmodified current official SDK (`@xdevplatform/xdk`) drives
|
|
73
76
|
`users.getMe()` against the twin through its own public `baseUrl` config
|
|
74
77
|
(`xidentity-sdk.integration.test.ts`); the OAuth legs its hardcoded hosts cannot re-aim are
|
|
@@ -52,7 +52,7 @@ import { XIdentityBudgetError, XIDENTITY_BUDGET_BURST_CEILING } from "./xidentit
|
|
|
52
52
|
import { liveXIdentityExecute, mapUsersMeAccount, pullXIdentity, pushPendingXIdentityActions, syncXIdentityFromReal, } from "./xidentity-connector.js";
|
|
53
53
|
import { pkceS256 } from "./xidentity-pkce.js";
|
|
54
54
|
import { DEFAULT_ACCOUNTS, DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET, DEFAULT_PUBLIC_CLIENT_ID, defaultRedirectUris } from "./xidentity-store.js";
|
|
55
|
-
import { handleXIdentityTwinRequest } from "./xidentity-twin.js";
|
|
55
|
+
import { handleXIdentityTwinRequest, X_DEFAULT_PROFILE_IMAGE_URL } from "./xidentity-twin.js";
|
|
56
56
|
const AT = '2026-02-01T00:00:00.000Z';
|
|
57
57
|
const atPlus = (seconds) => new Date(Date.parse(AT) + seconds * 1000).toISOString();
|
|
58
58
|
/** The origin this harness's world serves the twin at. The seeded demo clients' callbacks are
|
|
@@ -502,17 +502,41 @@ export const XIDENTITY_CAPABILITIES = [
|
|
|
502
502
|
&& d['created_at'] === ADA.createdAt
|
|
503
503
|
&& d['description'] === ADA.description
|
|
504
504
|
&& d['location'] === ADA.location
|
|
505
|
-
&& d['profile_image_url'] ===
|
|
505
|
+
&& d['profile_image_url'] === X_DEFAULT_PROFILE_IMAGE_URL // Ada is seeded without a photo
|
|
506
506
|
&& d['protected'] === false
|
|
507
507
|
&& d['url'] === ADA.url
|
|
508
508
|
&& d['verified'] === true
|
|
509
509
|
&& d['verified_type'] === 'blue';
|
|
510
510
|
})),
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
511
|
+
// Every X account has a profile image: X's default avatar until the person sets a photo (the x pack's
|
|
512
|
+
// `X_DEFAULT_PROFILE_IMAGE_URL`; X's own payload example serves it for a photo-less user). Postiz's X
|
|
513
|
+
// connect reads it from v2.me and recorded an empty picture for @acmeshoes while the x pack, asked
|
|
514
|
+
// for the same person, answered the default. A PULLED persona the reply told nothing about stays
|
|
515
|
+
// without one (the pull asked; a withheld field is not "no photo"). That both packs answer one person
|
|
516
|
+
// alike is xidentity-x-avatar.integration.test.ts: both packs on one root, which a shipped verify
|
|
517
|
+
// does not reach (A3).
|
|
518
|
+
done('xidentity.users_me.default_profile_image', 'users_me', "user.fields=profile_image_url for a persona seeded without a photo is X's default avatar URL (abs.twimg.com default_profile_normal.png), the same answer the x pack gives for the same person; a seeded photo is served as set, the field stays out unless asked for, and a pulled persona whose reply withheld it is not given one (the x pack's answer for the same person: xidentity-x-avatar.integration.test.ts)", 'api', 'common', () => withRoot(async (h) => {
|
|
519
|
+
const PLAIN = { id: '1790000000000000001', username: 'acmeshoes', name: 'Acme Shoes' };
|
|
520
|
+
const PHOTO = { id: '1790000000000000002', username: 'pictured', name: 'Pictured', profile_image_url: 'https://pbs.twimg.com/profile_images/1/pictured_normal.jpg' };
|
|
521
|
+
await h({ m: 'GET', p: '/i/oauth2/authorize' }); // seed the world (clients, session)
|
|
522
|
+
for (const person of [PLAIN, PHOTO])
|
|
523
|
+
await h({ m: 'POST', p: '/_twin/accounts', b: JSON.stringify(person) });
|
|
524
|
+
await pullXIdentity(async () => ({ data: { id: '4444', username: 'withheld' } }), h.root, '2026-02-01T00:00:01.000Z');
|
|
525
|
+
const meOf = async (accountId, query) => {
|
|
526
|
+
await h({ m: 'POST', p: '/_twin/session', b: JSON.stringify({ account_id: accountId }) });
|
|
527
|
+
const { tokens } = await fullFlow(h);
|
|
528
|
+
const r = await h({ m: 'GET', p: `/2/users/me${query}`, h: { authorization: `Bearer ${tokens['access_token']}` } });
|
|
529
|
+
return r.status === 200 ? body(r).data : undefined;
|
|
530
|
+
};
|
|
531
|
+
const plain = await meOf(PLAIN.id, '?user.fields=profile_image_url');
|
|
532
|
+
const plainBare = await meOf(PLAIN.id, '');
|
|
533
|
+
const photo = await meOf(PHOTO.id, '?user.fields=profile_image_url,name');
|
|
534
|
+
const withheld = await meOf('4444', '?user.fields=profile_image_url');
|
|
535
|
+
return plain?.['profile_image_url'] === X_DEFAULT_PROFILE_IMAGE_URL
|
|
536
|
+
&& plainBare !== undefined && !('profile_image_url' in plainBare)
|
|
537
|
+
&& photo?.['profile_image_url'] === PHOTO.profile_image_url
|
|
538
|
+
&& withheld?.['id'] === '4444' && !('profile_image_url' in withheld);
|
|
539
|
+
})),
|
|
516
540
|
done('xidentity.users_me.public_metrics', 'users_me', 'user.fields=public_metrics returns the metrics object with the vendor\'s six counters', 'api', 'common', () => withRoot(async (h) => {
|
|
517
541
|
const { tokens } = await fullFlow(h);
|
|
518
542
|
const r = await h({ m: 'GET', p: '/2/users/me?user.fields=public_metrics', h: { authorization: `Bearer ${tokens['access_token']}` } });
|
|
@@ -41,6 +41,15 @@ export type XIdentityResponse = XResponse;
|
|
|
41
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
42
|
/** The `expansions` enum — UserExpansionsParameter (2.167), verbatim. */
|
|
43
43
|
export declare const USER_EXPANSIONS: readonly ["affiliation", "most_recent_post_id", "pinned_post_id"];
|
|
44
|
+
/** The picture X shows for an account that never uploaded one. Every X account has a profile image;
|
|
45
|
+
* until the person sets a photo it is this URL — the `profile_image_url_https` X serves for such a
|
|
46
|
+
* user (docs.x.com/x-api/account-activity/introduction, its payload example's "Harrison Test",
|
|
47
|
+
* fetched 2026-09-28), and the URL answers X's grey silhouette (observed 2026-09-28:
|
|
48
|
+
* `curl -sS -o /dev/null -w '%{http_code} %{content_type}'` over it gives `200 image/png`, 48×48).
|
|
49
|
+
* TRANSCRIBED from the x pack's `X_DEFAULT_PROFILE_IMAGE_URL` (packages/twin/x/src/x-twin.ts), not
|
|
50
|
+
* imported (A3); xidentity-x-avatar.integration.test.ts asks both packs for the same person
|
|
51
|
+
* on one root and fails if they part. */
|
|
52
|
+
export declare const X_DEFAULT_PROFILE_IMAGE_URL = "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png";
|
|
44
53
|
/** The scopes GET /2/users/me demands — the OpenAPI operation's OAuth2UserToken entry lists BOTH. */
|
|
45
54
|
export declare const USERS_ME_REQUIRED_SCOPES: readonly ["tweet.read", "users.read"];
|
|
46
55
|
/** Every VENDOR endpoint this twin claims to serve. The conformance check drives one real request
|
|
@@ -558,12 +558,31 @@ export const USER_FIELDS = [
|
|
|
558
558
|
];
|
|
559
559
|
/** The `expansions` enum — UserExpansionsParameter (2.167), verbatim. */
|
|
560
560
|
export const USER_EXPANSIONS = ['affiliation', 'most_recent_post_id', 'pinned_post_id'];
|
|
561
|
+
/** The picture X shows for an account that never uploaded one. Every X account has a profile image;
|
|
562
|
+
* until the person sets a photo it is this URL — the `profile_image_url_https` X serves for such a
|
|
563
|
+
* user (docs.x.com/x-api/account-activity/introduction, its payload example's "Harrison Test",
|
|
564
|
+
* fetched 2026-09-28), and the URL answers X's grey silhouette (observed 2026-09-28:
|
|
565
|
+
* `curl -sS -o /dev/null -w '%{http_code} %{content_type}'` over it gives `200 image/png`, 48×48).
|
|
566
|
+
* TRANSCRIBED from the x pack's `X_DEFAULT_PROFILE_IMAGE_URL` (packages/twin/x/src/x-twin.ts), not
|
|
567
|
+
* imported (A3); xidentity-x-avatar.integration.test.ts asks both packs for the same person
|
|
568
|
+
* on one root and fails if they part. */
|
|
569
|
+
export const X_DEFAULT_PROFILE_IMAGE_URL = 'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png';
|
|
570
|
+
/** A persona's `profile_image_url`: the photo on the row, else X's default for a persona the twin
|
|
571
|
+
* seeded (a seeded persona without a photo has none, and X answers the default for that); a PULLED
|
|
572
|
+
* persona without one is undefined, because the pull asked for the field and a reply that withheld
|
|
573
|
+
* it (partial errors) says nothing about the real account's picture. */
|
|
574
|
+
function profileImageOf(account) {
|
|
575
|
+
if (typeof account.profileImageUrl === 'string' && account.profileImageUrl !== '')
|
|
576
|
+
return account.profileImageUrl;
|
|
577
|
+
return account.pulled === true ? undefined : X_DEFAULT_PROFILE_IMAGE_URL;
|
|
578
|
+
}
|
|
561
579
|
/** The scopes GET /2/users/me demands — the OpenAPI operation's OAuth2UserToken entry lists BOTH. */
|
|
562
580
|
export const USERS_ME_REQUIRED_SCOPES = ['tweet.read', 'users.read'];
|
|
563
581
|
/** Map an account row to the vendor's User object for a requested field set. Default fields are
|
|
564
582
|
* id, name, username (the OpenAPI/docs default); everything else appears only when asked for and
|
|
565
583
|
* only when the twin models it — a modelled-but-absent optional field is OMITTED, matching the
|
|
566
|
-
* spec (no User property is `required`)
|
|
584
|
+
* spec (no User property is `required`) — except `profile_image_url`, which every X account has
|
|
585
|
+
* (X's default avatar until a photo is set; `profileImageOf`). Unmodelled enum values (`connection_status`,
|
|
567
586
|
* `receives_your_dm`, …) are accepted and omitted; each is a named manifest todo, not silent.
|
|
568
587
|
* `confirmed_email` additionally demands the `users.email` scope (X's April-2025 announcement —
|
|
569
588
|
* see xidentity-scopes.ts); OMISSION when the scope is missing is EXTRAPOLATION, pinned by
|
|
@@ -584,8 +603,11 @@ function renderUser(account, fields, heldScopes) {
|
|
|
584
603
|
out['description'] = account.description;
|
|
585
604
|
if (want.has('location') && typeof account.location === 'string')
|
|
586
605
|
out['location'] = account.location;
|
|
587
|
-
if (want.has('profile_image_url')
|
|
588
|
-
|
|
606
|
+
if (want.has('profile_image_url')) {
|
|
607
|
+
const picture = profileImageOf(account);
|
|
608
|
+
if (picture !== undefined)
|
|
609
|
+
out['profile_image_url'] = picture;
|
|
610
|
+
}
|
|
589
611
|
if (want.has('protected'))
|
|
590
612
|
out['protected'] = account.protectedAccount === true;
|
|
591
613
|
if (want.has('public_metrics') && account.publicMetrics && typeof account.publicMetrics === 'object')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-xidentity",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local X (Twitter) identity twin — the real x.com authorize screen, the full OAuth 2.0 authorization-code + PKCE round trip (token, refresh, revoke), and GET /2/users/me with X's real field/expansion/problem envelopes, so an unmodified X client completes sign-in-with-X against it. Built on @volter/world-core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"react-dom": "^19.2.7"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@volter/world-core": "2.0.
|
|
59
|
+
"@volter/world-core": "2.0.4"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@volter/world-core": "2.0.
|
|
62
|
+
"@volter/world-core": "2.0.4",
|
|
63
63
|
"@volter/world-tooling": "0.1.0",
|
|
64
64
|
"@xdevplatform/xdk": "^0.6.6",
|
|
65
65
|
"@types/bun": "^1.2.20",
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
} from './xidentity-connector.ts';
|
|
60
60
|
import { pkceS256 } from './xidentity-pkce.ts';
|
|
61
61
|
import { DEFAULT_ACCOUNTS, DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET, DEFAULT_PUBLIC_CLIENT_ID, defaultRedirectUris } from './xidentity-store.ts';
|
|
62
|
-
import { handleXIdentityTwinRequest, USER_FIELDS, type XResponse } from './xidentity-twin.ts';
|
|
62
|
+
import { handleXIdentityTwinRequest, USER_FIELDS, X_DEFAULT_PROFILE_IMAGE_URL, type XResponse } from './xidentity-twin.ts';
|
|
63
63
|
|
|
64
64
|
type Step = { m: string; p: string; b?: string; h?: Record<string, string>; at?: string };
|
|
65
65
|
type Body = Record<string, any>;
|
|
@@ -584,17 +584,41 @@ export const XIDENTITY_CAPABILITIES: CapabilitySpec[] = [
|
|
|
584
584
|
&& d['created_at'] === ADA.createdAt
|
|
585
585
|
&& d['description'] === ADA.description
|
|
586
586
|
&& d['location'] === ADA.location
|
|
587
|
-
&& d['profile_image_url'] ===
|
|
587
|
+
&& d['profile_image_url'] === X_DEFAULT_PROFILE_IMAGE_URL // Ada is seeded without a photo
|
|
588
588
|
&& d['protected'] === false
|
|
589
589
|
&& d['url'] === ADA.url
|
|
590
590
|
&& d['verified'] === true
|
|
591
591
|
&& d['verified_type'] === 'blue';
|
|
592
592
|
})),
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
|
|
593
|
+
// Every X account has a profile image: X's default avatar until the person sets a photo (the x pack's
|
|
594
|
+
// `X_DEFAULT_PROFILE_IMAGE_URL`; X's own payload example serves it for a photo-less user). Postiz's X
|
|
595
|
+
// connect reads it from v2.me and recorded an empty picture for @acmeshoes while the x pack, asked
|
|
596
|
+
// for the same person, answered the default. A PULLED persona the reply told nothing about stays
|
|
597
|
+
// without one (the pull asked; a withheld field is not "no photo"). That both packs answer one person
|
|
598
|
+
// alike is xidentity-x-avatar.integration.test.ts: both packs on one root, which a shipped verify
|
|
599
|
+
// does not reach (A3).
|
|
600
|
+
done('xidentity.users_me.default_profile_image', 'users_me', "user.fields=profile_image_url for a persona seeded without a photo is X's default avatar URL (abs.twimg.com default_profile_normal.png), the same answer the x pack gives for the same person; a seeded photo is served as set, the field stays out unless asked for, and a pulled persona whose reply withheld it is not given one (the x pack's answer for the same person: xidentity-x-avatar.integration.test.ts)", 'api', 'common', () =>
|
|
601
|
+
withRoot(async (h) => {
|
|
602
|
+
const PLAIN = { id: '1790000000000000001', username: 'acmeshoes', name: 'Acme Shoes' };
|
|
603
|
+
const PHOTO = { id: '1790000000000000002', username: 'pictured', name: 'Pictured', profile_image_url: 'https://pbs.twimg.com/profile_images/1/pictured_normal.jpg' };
|
|
604
|
+
await h({ m: 'GET', p: '/i/oauth2/authorize' }); // seed the world (clients, session)
|
|
605
|
+
for (const person of [PLAIN, PHOTO]) await h({ m: 'POST', p: '/_twin/accounts', b: JSON.stringify(person) });
|
|
606
|
+
await pullXIdentity(async () => ({ data: { id: '4444', username: 'withheld' } }), h.root, '2026-02-01T00:00:01.000Z');
|
|
607
|
+
const meOf = async (accountId: string, query: string): Promise<Body | undefined> => {
|
|
608
|
+
await h({ m: 'POST', p: '/_twin/session', b: JSON.stringify({ account_id: accountId }) });
|
|
609
|
+
const { tokens } = await fullFlow(h);
|
|
610
|
+
const r = await h({ m: 'GET', p: `/2/users/me${query}`, h: { authorization: `Bearer ${tokens['access_token']}` } });
|
|
611
|
+
return r.status === 200 ? body(r).data : undefined;
|
|
612
|
+
};
|
|
613
|
+
const plain = await meOf(PLAIN.id, '?user.fields=profile_image_url');
|
|
614
|
+
const plainBare = await meOf(PLAIN.id, '');
|
|
615
|
+
const photo = await meOf(PHOTO.id, '?user.fields=profile_image_url,name');
|
|
616
|
+
const withheld = await meOf('4444', '?user.fields=profile_image_url');
|
|
617
|
+
return plain?.['profile_image_url'] === X_DEFAULT_PROFILE_IMAGE_URL
|
|
618
|
+
&& plainBare !== undefined && !('profile_image_url' in plainBare)
|
|
619
|
+
&& photo?.['profile_image_url'] === PHOTO.profile_image_url
|
|
620
|
+
&& withheld?.['id'] === '4444' && !('profile_image_url' in withheld);
|
|
621
|
+
})),
|
|
598
622
|
done('xidentity.users_me.public_metrics', 'users_me', 'user.fields=public_metrics returns the metrics object with the vendor\'s six counters', 'api', 'common', () =>
|
|
599
623
|
withRoot(async (h) => {
|
|
600
624
|
const { tokens } = await fullFlow(h);
|
package/src/xidentity-twin.ts
CHANGED
|
@@ -639,13 +639,33 @@ export const USER_FIELDS = [
|
|
|
639
639
|
/** The `expansions` enum — UserExpansionsParameter (2.167), verbatim. */
|
|
640
640
|
export const USER_EXPANSIONS = ['affiliation', 'most_recent_post_id', 'pinned_post_id'] as const;
|
|
641
641
|
|
|
642
|
+
/** The picture X shows for an account that never uploaded one. Every X account has a profile image;
|
|
643
|
+
* until the person sets a photo it is this URL — the `profile_image_url_https` X serves for such a
|
|
644
|
+
* user (docs.x.com/x-api/account-activity/introduction, its payload example's "Harrison Test",
|
|
645
|
+
* fetched 2026-09-28), and the URL answers X's grey silhouette (observed 2026-09-28:
|
|
646
|
+
* `curl -sS -o /dev/null -w '%{http_code} %{content_type}'` over it gives `200 image/png`, 48×48).
|
|
647
|
+
* TRANSCRIBED from the x pack's `X_DEFAULT_PROFILE_IMAGE_URL` (packages/twin/x/src/x-twin.ts), not
|
|
648
|
+
* imported (A3); xidentity-x-avatar.integration.test.ts asks both packs for the same person
|
|
649
|
+
* on one root and fails if they part. */
|
|
650
|
+
export const X_DEFAULT_PROFILE_IMAGE_URL = 'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png';
|
|
651
|
+
|
|
652
|
+
/** A persona's `profile_image_url`: the photo on the row, else X's default for a persona the twin
|
|
653
|
+
* seeded (a seeded persona without a photo has none, and X answers the default for that); a PULLED
|
|
654
|
+
* persona without one is undefined, because the pull asked for the field and a reply that withheld
|
|
655
|
+
* it (partial errors) says nothing about the real account's picture. */
|
|
656
|
+
function profileImageOf(account: Row): string | undefined {
|
|
657
|
+
if (typeof account.profileImageUrl === 'string' && account.profileImageUrl !== '') return account.profileImageUrl;
|
|
658
|
+
return account.pulled === true ? undefined : X_DEFAULT_PROFILE_IMAGE_URL;
|
|
659
|
+
}
|
|
660
|
+
|
|
642
661
|
/** The scopes GET /2/users/me demands — the OpenAPI operation's OAuth2UserToken entry lists BOTH. */
|
|
643
662
|
export const USERS_ME_REQUIRED_SCOPES = ['tweet.read', 'users.read'] as const;
|
|
644
663
|
|
|
645
664
|
/** Map an account row to the vendor's User object for a requested field set. Default fields are
|
|
646
665
|
* id, name, username (the OpenAPI/docs default); everything else appears only when asked for and
|
|
647
666
|
* only when the twin models it — a modelled-but-absent optional field is OMITTED, matching the
|
|
648
|
-
* spec (no User property is `required`)
|
|
667
|
+
* spec (no User property is `required`) — except `profile_image_url`, which every X account has
|
|
668
|
+
* (X's default avatar until a photo is set; `profileImageOf`). Unmodelled enum values (`connection_status`,
|
|
649
669
|
* `receives_your_dm`, …) are accepted and omitted; each is a named manifest todo, not silent.
|
|
650
670
|
* `confirmed_email` additionally demands the `users.email` scope (X's April-2025 announcement —
|
|
651
671
|
* see xidentity-scopes.ts); OMISSION when the scope is missing is EXTRAPOLATION, pinned by
|
|
@@ -663,7 +683,10 @@ function renderUser(account: Row, fields: readonly string[], heldScopes: readonl
|
|
|
663
683
|
if (want.has('created_at') && typeof account.createdAt === 'string') out['created_at'] = account.createdAt;
|
|
664
684
|
if (want.has('description') && typeof account.description === 'string') out['description'] = account.description;
|
|
665
685
|
if (want.has('location') && typeof account.location === 'string') out['location'] = account.location;
|
|
666
|
-
if (want.has('profile_image_url')
|
|
686
|
+
if (want.has('profile_image_url')) {
|
|
687
|
+
const picture = profileImageOf(account);
|
|
688
|
+
if (picture !== undefined) out['profile_image_url'] = picture;
|
|
689
|
+
}
|
|
667
690
|
if (want.has('protected')) out['protected'] = account.protectedAccount === true;
|
|
668
691
|
if (want.has('public_metrics') && account.publicMetrics && typeof account.publicMetrics === 'object') out['public_metrics'] = account.publicMetrics;
|
|
669
692
|
if (want.has('url') && typeof account.url === 'string') out['url'] = account.url;
|