@shware/http 2.10.4 → 2.11.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.
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/google-one-tap/index.ts
21
21
  var google_one_tap_exports = {};
22
22
  __export(google_one_tap_exports, {
23
- prompt: () => prompt
23
+ prompt: () => prompt,
24
+ requestCode: () => requestCode
24
25
  });
25
26
  module.exports = __toCommonJS(google_one_tap_exports);
26
27
  var script = null;
@@ -48,6 +49,9 @@ function loadScript() {
48
49
  document.head.appendChild(script);
49
50
  });
50
51
  }
52
+ function isFedcmSupported() {
53
+ return typeof window !== "undefined" && "IdentityCredential" in window;
54
+ }
51
55
  async function prompt({
52
56
  nonce,
53
57
  client_id,
@@ -55,47 +59,86 @@ async function prompt({
55
59
  use_fedcm_for_prompt = true,
56
60
  cancel_on_tap_outside = false
57
61
  }) {
58
- await loadScript();
62
+ if (use_fedcm_for_prompt !== false && !isFedcmSupported()) {
63
+ return { authorized: false, unsupported: true, reason: "fedcm_unsupported" };
64
+ }
65
+ try {
66
+ await loadScript();
67
+ } catch {
68
+ return { authorized: false, unsupported: true, reason: "script_load_failed" };
69
+ }
59
70
  return new Promise((resolve) => {
60
71
  let settled = false;
61
- window.google.accounts.id.initialize({
62
- ux_mode: "popup",
63
- context: "signin",
64
- auto_select,
65
- nonce,
66
- client_id,
67
- use_fedcm_for_prompt,
68
- cancel_on_tap_outside,
69
- callback: (credential) => {
70
- if (settled) return;
71
- settled = true;
72
- resolve({ authorized: true, credential });
73
- },
74
- native_callback: (credential) => {
72
+ const settle = (result) => {
73
+ if (settled) return;
74
+ settled = true;
75
+ resolve(result);
76
+ };
77
+ try {
78
+ window.google.accounts.id.initialize({
79
+ ux_mode: "popup",
80
+ context: "signin",
81
+ auto_select,
82
+ nonce,
83
+ client_id,
84
+ use_fedcm_for_prompt,
85
+ cancel_on_tap_outside,
86
+ callback: (credential) => settle({ authorized: true, credential }),
87
+ native_callback: (credential) => settle({ authorized: true, credential })
88
+ });
89
+ window.google.accounts.id.prompt((notification) => {
75
90
  if (settled) return;
76
- settled = true;
77
- resolve({ authorized: true, credential });
78
- }
79
- });
80
- window.google.accounts.id.prompt((notification) => {
91
+ if (notification.isSkippedMoment() || notification.isDismissedMoment() && notification.getDismissedReason() !== "credential_returned") {
92
+ settle({
93
+ authorized: false,
94
+ moment: {
95
+ momentType: notification.getMomentType(),
96
+ skipped: notification.isSkippedMoment(),
97
+ dismissed: notification.isDismissedMoment(),
98
+ dismissedReason: notification.getDismissedReason()
99
+ }
100
+ });
101
+ }
102
+ });
103
+ } catch {
104
+ settle({ authorized: false, unsupported: true, reason: "prompt_error" });
105
+ }
106
+ });
107
+ }
108
+ async function requestCode({
109
+ client_id,
110
+ scope = "openid email profile",
111
+ ux_mode = "popup",
112
+ redirect_uri,
113
+ state,
114
+ login_hint,
115
+ hd
116
+ }) {
117
+ await loadScript();
118
+ return new Promise((resolve) => {
119
+ let settled = false;
120
+ const settle = (result) => {
81
121
  if (settled) return;
82
- if (notification.isSkippedMoment() || notification.isDismissedMoment() && notification.getDismissedReason() !== "credential_returned") {
83
- settled = true;
84
- resolve({
85
- authorized: false,
86
- moment: {
87
- momentType: notification.getMomentType(),
88
- skipped: notification.isSkippedMoment(),
89
- dismissed: notification.isDismissedMoment(),
90
- dismissedReason: notification.getDismissedReason()
91
- }
92
- });
93
- }
122
+ settled = true;
123
+ resolve(result);
124
+ };
125
+ const client = window.google.accounts.oauth2.initCodeClient({
126
+ client_id,
127
+ scope,
128
+ ux_mode,
129
+ redirect_uri,
130
+ state,
131
+ login_hint,
132
+ hd,
133
+ callback: (response) => settle(response.code ? { ok: true, response } : { ok: false, error: response }),
134
+ error_callback: (error) => settle({ ok: false, error })
94
135
  });
136
+ client.requestCode();
95
137
  });
96
138
  }
97
139
  // Annotate the CommonJS export names for ESM import in node:
98
140
  0 && (module.exports = {
99
- prompt
141
+ prompt,
142
+ requestCode
100
143
  });
101
144
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/google-one-tap/index.ts"],"sourcesContent":["import type { CredentialResponse, GoogleAccounts, PromptMomentNotification } from './types';\n\ndeclare global {\n interface Window {\n google: {\n accounts: GoogleAccounts;\n };\n }\n}\n\nexport type Props = {\n /**\n * Opaque nonce string forwarded as-is to Google. Google echoes it back in\n * the ID Token's `nonce` claim per the OIDC spec; the caller is responsible\n * for any hashing/encoding required by its verifier.\n */\n nonce?: string;\n client_id: string;\n auto_select?: boolean;\n cancel_on_tap_outside?: boolean;\n use_fedcm_for_prompt?: boolean;\n};\n\nexport type PromptMoment = {\n skipped: boolean;\n dismissed: boolean;\n momentType: ReturnType<PromptMomentNotification['getMomentType']>;\n dismissedReason: ReturnType<PromptMomentNotification['getDismissedReason']>;\n};\n\nexport type PromptResult =\n | { authorized: true; credential: CredentialResponse }\n | { authorized: false; moment: PromptMoment };\n\nlet script: HTMLScriptElement | null = null;\n\nfunction loadScript(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (script) {\n if (window.google?.accounts?.id) {\n resolve();\n } else {\n script.addEventListener('load', () => resolve());\n script.addEventListener('error', () =>\n reject(new Error('Failed to load Google One Tap script'))\n );\n }\n return;\n }\n\n script = document.createElement('script');\n script.id = 'google-one-tap';\n script.async = true;\n script.defer = true;\n script.src = 'https://accounts.google.com/gsi/client';\n script.onload = () => resolve();\n script.onerror = () => reject(new Error('Failed to load Google One Tap script'));\n\n document.head.appendChild(script);\n });\n}\n\n/** debug: chrome://settings/content/federatedIdentityApi */\nexport async function prompt({\n nonce,\n client_id,\n auto_select = false,\n use_fedcm_for_prompt = true,\n cancel_on_tap_outside = false,\n}: Props): Promise<PromptResult> {\n await loadScript();\n\n return new Promise<PromptResult>((resolve) => {\n let settled = false;\n\n window.google.accounts.id.initialize({\n ux_mode: 'popup',\n context: 'signin',\n auto_select,\n nonce,\n client_id,\n use_fedcm_for_prompt,\n cancel_on_tap_outside,\n callback: (credential) => {\n if (settled) return;\n settled = true;\n resolve({ authorized: true, credential });\n },\n native_callback: (credential) => {\n if (settled) return;\n settled = true;\n resolve({ authorized: true, credential });\n },\n });\n\n window.google.accounts.id.prompt((notification) => {\n if (settled) return;\n\n if (\n notification.isSkippedMoment() ||\n (notification.isDismissedMoment() &&\n notification.getDismissedReason() !== 'credential_returned')\n ) {\n settled = true;\n resolve({\n authorized: false,\n moment: {\n momentType: notification.getMomentType(),\n skipped: notification.isSkippedMoment(),\n dismissed: notification.isDismissedMoment(),\n dismissedReason: notification.getDismissedReason(),\n },\n });\n }\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCA,IAAI,SAAmC;AAEvC,SAAS,aAA4B;AACnC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,QAAQ;AACV,UAAI,OAAO,QAAQ,UAAU,IAAI;AAC/B,gBAAQ;AAAA,MACV,OAAO;AACL,eAAO,iBAAiB,QAAQ,MAAM,QAAQ,CAAC;AAC/C,eAAO;AAAA,UAAiB;AAAA,UAAS,MAC/B,OAAO,IAAI,MAAM,sCAAsC,CAAC;AAAA,QAC1D;AAAA,MACF;AACA;AAAA,IACF;AAEA,aAAS,SAAS,cAAc,QAAQ;AACxC,WAAO,KAAK;AACZ,WAAO,QAAQ;AACf,WAAO,QAAQ;AACf,WAAO,MAAM;AACb,WAAO,SAAS,MAAM,QAAQ;AAC9B,WAAO,UAAU,MAAM,OAAO,IAAI,MAAM,sCAAsC,CAAC;AAE/E,aAAS,KAAK,YAAY,MAAM;AAAA,EAClC,CAAC;AACH;AAGA,eAAsB,OAAO;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,uBAAuB;AAAA,EACvB,wBAAwB;AAC1B,GAAiC;AAC/B,QAAM,WAAW;AAEjB,SAAO,IAAI,QAAsB,CAAC,YAAY;AAC5C,QAAI,UAAU;AAEd,WAAO,OAAO,SAAS,GAAG,WAAW;AAAA,MACnC,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,eAAe;AACxB,YAAI,QAAS;AACb,kBAAU;AACV,gBAAQ,EAAE,YAAY,MAAM,WAAW,CAAC;AAAA,MAC1C;AAAA,MACA,iBAAiB,CAAC,eAAe;AAC/B,YAAI,QAAS;AACb,kBAAU;AACV,gBAAQ,EAAE,YAAY,MAAM,WAAW,CAAC;AAAA,MAC1C;AAAA,IACF,CAAC;AAED,WAAO,OAAO,SAAS,GAAG,OAAO,CAAC,iBAAiB;AACjD,UAAI,QAAS;AAEb,UACE,aAAa,gBAAgB,KAC5B,aAAa,kBAAkB,KAC9B,aAAa,mBAAmB,MAAM,uBACxC;AACA,kBAAU;AACV,gBAAQ;AAAA,UACN,YAAY;AAAA,UACZ,QAAQ;AAAA,YACN,YAAY,aAAa,cAAc;AAAA,YACvC,SAAS,aAAa,gBAAgB;AAAA,YACtC,WAAW,aAAa,kBAAkB;AAAA,YAC1C,iBAAiB,aAAa,mBAAmB;AAAA,UACnD;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/google-one-tap/index.ts"],"sourcesContent":["import type {\n ClientConfigError,\n CodeResponse,\n CredentialResponse,\n GoogleAccounts,\n PromptMomentNotification,\n} from './types';\n\ndeclare global {\n interface Window {\n google: {\n accounts: GoogleAccounts;\n };\n }\n}\n\nexport type Props = {\n /**\n * Opaque nonce string forwarded as-is to Google. Google echoes it back in\n * the ID Token's `nonce` claim per the OIDC spec; the caller is responsible\n * for any hashing/encoding required by its verifier.\n */\n nonce?: string;\n client_id: string;\n auto_select?: boolean;\n cancel_on_tap_outside?: boolean;\n use_fedcm_for_prompt?: boolean;\n};\n\nexport type PromptMoment = {\n skipped: boolean;\n dismissed: boolean;\n momentType: ReturnType<PromptMomentNotification['getMomentType']>;\n dismissedReason: ReturnType<PromptMomentNotification['getDismissedReason']>;\n};\n\n/**\n * Why prompt() could not run One Tap, so the caller can decide to fall back\n * to requestCode().\n * - `fedcm_unsupported`: browser has no FedCM (Safari/Firefox/pre-117 Chrome).\n * - `script_load_failed`: the GSI script failed to load (network/CSP).\n * - `prompt_error`: GIS threw synchronously while initializing/prompting.\n */\nexport type UnsupportedReason = 'fedcm_unsupported' | 'script_load_failed' | 'prompt_error';\n\nexport type PromptResult =\n | { authorized: true; credential: CredentialResponse }\n | { authorized: false; moment: PromptMoment }\n | { authorized: false; unsupported: true; reason: UnsupportedReason };\n\nlet script: HTMLScriptElement | null = null;\n\nfunction loadScript(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (script) {\n if (window.google?.accounts?.id) {\n resolve();\n } else {\n script.addEventListener('load', () => resolve());\n script.addEventListener('error', () =>\n reject(new Error('Failed to load Google One Tap script'))\n );\n }\n return;\n }\n\n script = document.createElement('script');\n script.id = 'google-one-tap';\n script.async = true;\n script.defer = true;\n script.src = 'https://accounts.google.com/gsi/client';\n script.onload = () => resolve();\n script.onerror = () => reject(new Error('Failed to load Google One Tap script'));\n\n document.head.appendChild(script);\n });\n}\n\n/** FedCM feature detection — `IdentityCredential` is absent on Safari, Firefox and pre-117 Chrome. */\nfunction isFedcmSupported(): boolean {\n return typeof window !== 'undefined' && 'IdentityCredential' in window;\n}\n\n/** debug: chrome://settings/content/federatedIdentityApi */\nexport async function prompt({\n nonce,\n client_id,\n auto_select = false,\n use_fedcm_for_prompt = true,\n cancel_on_tap_outside = false,\n}: Props): Promise<PromptResult> {\n // FedCM is mandatory for One Tap on capable browsers (GIS completed the\n // migration in early 2025) and there is no viable legacy fallback anymore.\n // When the caller wants FedCM but the browser lacks it, bail early instead\n // of letting GIS throw an async `NotSupportedError: Missing request type`\n // from navigator.credentials.get. Callers should fall back to requestCode().\n if (use_fedcm_for_prompt !== false && !isFedcmSupported()) {\n return { authorized: false, unsupported: true, reason: 'fedcm_unsupported' };\n }\n\n try {\n await loadScript();\n } catch {\n return { authorized: false, unsupported: true, reason: 'script_load_failed' };\n }\n\n return new Promise<PromptResult>((resolve) => {\n let settled = false;\n const settle = (result: PromptResult) => {\n if (settled) return;\n settled = true;\n resolve(result);\n };\n\n try {\n window.google.accounts.id.initialize({\n ux_mode: 'popup',\n context: 'signin',\n auto_select,\n nonce,\n client_id,\n use_fedcm_for_prompt,\n cancel_on_tap_outside,\n callback: (credential) => settle({ authorized: true, credential }),\n native_callback: (credential) => settle({ authorized: true, credential }),\n });\n\n window.google.accounts.id.prompt((notification) => {\n if (settled) return;\n\n if (\n notification.isSkippedMoment() ||\n (notification.isDismissedMoment() &&\n notification.getDismissedReason() !== 'credential_returned')\n ) {\n settle({\n authorized: false,\n moment: {\n momentType: notification.getMomentType(),\n skipped: notification.isSkippedMoment(),\n dismissed: notification.isDismissedMoment(),\n dismissedReason: notification.getDismissedReason(),\n },\n });\n }\n });\n } catch {\n settle({ authorized: false, unsupported: true, reason: 'prompt_error' });\n }\n });\n}\n\nexport type CodeProps = {\n client_id: string;\n /**\n * Space-delimited OAuth scopes. Defaults to OpenID Connect scopes so the\n * backend can exchange the code for an ID token, mirroring One Tap's intent.\n */\n scope?: string;\n /** 'popup' resolves the returned promise; 'redirect' navigates away and never resolves. */\n ux_mode?: 'popup' | 'redirect';\n /** Required when ux_mode is 'redirect'; for 'popup' Google uses postMessage. */\n redirect_uri?: string;\n state?: string;\n login_hint?: string;\n hd?: string;\n};\n\nexport type CodeResult =\n | { ok: true; response: CodeResponse }\n | { ok: false; error: ClientConfigError | CodeResponse };\n\n/**\n * OAuth 2.0 authorization-code fallback for environments where One Tap / FedCM\n * is unavailable (i.e. prompt() resolved with `unsupported: true`). It reuses\n * the same GSI script, so there is no extra dependency.\n *\n * Unlike One Tap it returns an authorization `code` for the backend to\n * exchange, not an ID token, and it must be triggered by a user gesture or the\n * popup may be blocked.\n */\nexport async function requestCode({\n client_id,\n scope = 'openid email profile',\n ux_mode = 'popup',\n redirect_uri,\n state,\n login_hint,\n hd,\n}: CodeProps): Promise<CodeResult> {\n await loadScript();\n\n return new Promise<CodeResult>((resolve) => {\n let settled = false;\n const settle = (result: CodeResult) => {\n if (settled) return;\n settled = true;\n resolve(result);\n };\n\n const client = window.google.accounts.oauth2.initCodeClient({\n client_id,\n scope,\n ux_mode,\n redirect_uri,\n state,\n login_hint,\n hd,\n callback: (response) =>\n settle(response.code ? { ok: true, response } : { ok: false, error: response }),\n error_callback: (error) => settle({ ok: false, error }),\n });\n\n client.requestCode();\n });\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { GoogleAccounts, PromptMomentNotification, CredentialResponse } from './types.cjs';
1
+ import { GoogleAccounts, PromptMomentNotification, CredentialResponse, CodeResponse, ClientConfigError } from './types.cjs';
2
2
 
3
3
  declare global {
4
4
  interface Window {
@@ -25,14 +25,58 @@ type PromptMoment = {
25
25
  momentType: ReturnType<PromptMomentNotification['getMomentType']>;
26
26
  dismissedReason: ReturnType<PromptMomentNotification['getDismissedReason']>;
27
27
  };
28
+ /**
29
+ * Why prompt() could not run One Tap, so the caller can decide to fall back
30
+ * to requestCode().
31
+ * - `fedcm_unsupported`: browser has no FedCM (Safari/Firefox/pre-117 Chrome).
32
+ * - `script_load_failed`: the GSI script failed to load (network/CSP).
33
+ * - `prompt_error`: GIS threw synchronously while initializing/prompting.
34
+ */
35
+ type UnsupportedReason = 'fedcm_unsupported' | 'script_load_failed' | 'prompt_error';
28
36
  type PromptResult = {
29
37
  authorized: true;
30
38
  credential: CredentialResponse;
31
39
  } | {
32
40
  authorized: false;
33
41
  moment: PromptMoment;
42
+ } | {
43
+ authorized: false;
44
+ unsupported: true;
45
+ reason: UnsupportedReason;
34
46
  };
35
47
  /** debug: chrome://settings/content/federatedIdentityApi */
36
48
  declare function prompt({ nonce, client_id, auto_select, use_fedcm_for_prompt, cancel_on_tap_outside, }: Props): Promise<PromptResult>;
49
+ type CodeProps = {
50
+ client_id: string;
51
+ /**
52
+ * Space-delimited OAuth scopes. Defaults to OpenID Connect scopes so the
53
+ * backend can exchange the code for an ID token, mirroring One Tap's intent.
54
+ */
55
+ scope?: string;
56
+ /** 'popup' resolves the returned promise; 'redirect' navigates away and never resolves. */
57
+ ux_mode?: 'popup' | 'redirect';
58
+ /** Required when ux_mode is 'redirect'; for 'popup' Google uses postMessage. */
59
+ redirect_uri?: string;
60
+ state?: string;
61
+ login_hint?: string;
62
+ hd?: string;
63
+ };
64
+ type CodeResult = {
65
+ ok: true;
66
+ response: CodeResponse;
67
+ } | {
68
+ ok: false;
69
+ error: ClientConfigError | CodeResponse;
70
+ };
71
+ /**
72
+ * OAuth 2.0 authorization-code fallback for environments where One Tap / FedCM
73
+ * is unavailable (i.e. prompt() resolved with `unsupported: true`). It reuses
74
+ * the same GSI script, so there is no extra dependency.
75
+ *
76
+ * Unlike One Tap it returns an authorization `code` for the backend to
77
+ * exchange, not an ID token, and it must be triggered by a user gesture or the
78
+ * popup may be blocked.
79
+ */
80
+ declare function requestCode({ client_id, scope, ux_mode, redirect_uri, state, login_hint, hd, }: CodeProps): Promise<CodeResult>;
37
81
 
38
- export { type PromptMoment, type PromptResult, type Props, prompt };
82
+ export { type CodeProps, type CodeResult, type PromptMoment, type PromptResult, type Props, type UnsupportedReason, prompt, requestCode };
@@ -1,4 +1,4 @@
1
- import { GoogleAccounts, PromptMomentNotification, CredentialResponse } from './types.js';
1
+ import { GoogleAccounts, PromptMomentNotification, CredentialResponse, CodeResponse, ClientConfigError } from './types.js';
2
2
 
3
3
  declare global {
4
4
  interface Window {
@@ -25,14 +25,58 @@ type PromptMoment = {
25
25
  momentType: ReturnType<PromptMomentNotification['getMomentType']>;
26
26
  dismissedReason: ReturnType<PromptMomentNotification['getDismissedReason']>;
27
27
  };
28
+ /**
29
+ * Why prompt() could not run One Tap, so the caller can decide to fall back
30
+ * to requestCode().
31
+ * - `fedcm_unsupported`: browser has no FedCM (Safari/Firefox/pre-117 Chrome).
32
+ * - `script_load_failed`: the GSI script failed to load (network/CSP).
33
+ * - `prompt_error`: GIS threw synchronously while initializing/prompting.
34
+ */
35
+ type UnsupportedReason = 'fedcm_unsupported' | 'script_load_failed' | 'prompt_error';
28
36
  type PromptResult = {
29
37
  authorized: true;
30
38
  credential: CredentialResponse;
31
39
  } | {
32
40
  authorized: false;
33
41
  moment: PromptMoment;
42
+ } | {
43
+ authorized: false;
44
+ unsupported: true;
45
+ reason: UnsupportedReason;
34
46
  };
35
47
  /** debug: chrome://settings/content/federatedIdentityApi */
36
48
  declare function prompt({ nonce, client_id, auto_select, use_fedcm_for_prompt, cancel_on_tap_outside, }: Props): Promise<PromptResult>;
49
+ type CodeProps = {
50
+ client_id: string;
51
+ /**
52
+ * Space-delimited OAuth scopes. Defaults to OpenID Connect scopes so the
53
+ * backend can exchange the code for an ID token, mirroring One Tap's intent.
54
+ */
55
+ scope?: string;
56
+ /** 'popup' resolves the returned promise; 'redirect' navigates away and never resolves. */
57
+ ux_mode?: 'popup' | 'redirect';
58
+ /** Required when ux_mode is 'redirect'; for 'popup' Google uses postMessage. */
59
+ redirect_uri?: string;
60
+ state?: string;
61
+ login_hint?: string;
62
+ hd?: string;
63
+ };
64
+ type CodeResult = {
65
+ ok: true;
66
+ response: CodeResponse;
67
+ } | {
68
+ ok: false;
69
+ error: ClientConfigError | CodeResponse;
70
+ };
71
+ /**
72
+ * OAuth 2.0 authorization-code fallback for environments where One Tap / FedCM
73
+ * is unavailable (i.e. prompt() resolved with `unsupported: true`). It reuses
74
+ * the same GSI script, so there is no extra dependency.
75
+ *
76
+ * Unlike One Tap it returns an authorization `code` for the backend to
77
+ * exchange, not an ID token, and it must be triggered by a user gesture or the
78
+ * popup may be blocked.
79
+ */
80
+ declare function requestCode({ client_id, scope, ux_mode, redirect_uri, state, login_hint, hd, }: CodeProps): Promise<CodeResult>;
37
81
 
38
- export { type PromptMoment, type PromptResult, type Props, prompt };
82
+ export { type CodeProps, type CodeResult, type PromptMoment, type PromptResult, type Props, type UnsupportedReason, prompt, requestCode };
@@ -24,6 +24,9 @@ function loadScript() {
24
24
  document.head.appendChild(script);
25
25
  });
26
26
  }
27
+ function isFedcmSupported() {
28
+ return typeof window !== "undefined" && "IdentityCredential" in window;
29
+ }
27
30
  async function prompt({
28
31
  nonce,
29
32
  client_id,
@@ -31,46 +34,85 @@ async function prompt({
31
34
  use_fedcm_for_prompt = true,
32
35
  cancel_on_tap_outside = false
33
36
  }) {
34
- await loadScript();
37
+ if (use_fedcm_for_prompt !== false && !isFedcmSupported()) {
38
+ return { authorized: false, unsupported: true, reason: "fedcm_unsupported" };
39
+ }
40
+ try {
41
+ await loadScript();
42
+ } catch {
43
+ return { authorized: false, unsupported: true, reason: "script_load_failed" };
44
+ }
35
45
  return new Promise((resolve) => {
36
46
  let settled = false;
37
- window.google.accounts.id.initialize({
38
- ux_mode: "popup",
39
- context: "signin",
40
- auto_select,
41
- nonce,
42
- client_id,
43
- use_fedcm_for_prompt,
44
- cancel_on_tap_outside,
45
- callback: (credential) => {
46
- if (settled) return;
47
- settled = true;
48
- resolve({ authorized: true, credential });
49
- },
50
- native_callback: (credential) => {
47
+ const settle = (result) => {
48
+ if (settled) return;
49
+ settled = true;
50
+ resolve(result);
51
+ };
52
+ try {
53
+ window.google.accounts.id.initialize({
54
+ ux_mode: "popup",
55
+ context: "signin",
56
+ auto_select,
57
+ nonce,
58
+ client_id,
59
+ use_fedcm_for_prompt,
60
+ cancel_on_tap_outside,
61
+ callback: (credential) => settle({ authorized: true, credential }),
62
+ native_callback: (credential) => settle({ authorized: true, credential })
63
+ });
64
+ window.google.accounts.id.prompt((notification) => {
51
65
  if (settled) return;
52
- settled = true;
53
- resolve({ authorized: true, credential });
54
- }
55
- });
56
- window.google.accounts.id.prompt((notification) => {
66
+ if (notification.isSkippedMoment() || notification.isDismissedMoment() && notification.getDismissedReason() !== "credential_returned") {
67
+ settle({
68
+ authorized: false,
69
+ moment: {
70
+ momentType: notification.getMomentType(),
71
+ skipped: notification.isSkippedMoment(),
72
+ dismissed: notification.isDismissedMoment(),
73
+ dismissedReason: notification.getDismissedReason()
74
+ }
75
+ });
76
+ }
77
+ });
78
+ } catch {
79
+ settle({ authorized: false, unsupported: true, reason: "prompt_error" });
80
+ }
81
+ });
82
+ }
83
+ async function requestCode({
84
+ client_id,
85
+ scope = "openid email profile",
86
+ ux_mode = "popup",
87
+ redirect_uri,
88
+ state,
89
+ login_hint,
90
+ hd
91
+ }) {
92
+ await loadScript();
93
+ return new Promise((resolve) => {
94
+ let settled = false;
95
+ const settle = (result) => {
57
96
  if (settled) return;
58
- if (notification.isSkippedMoment() || notification.isDismissedMoment() && notification.getDismissedReason() !== "credential_returned") {
59
- settled = true;
60
- resolve({
61
- authorized: false,
62
- moment: {
63
- momentType: notification.getMomentType(),
64
- skipped: notification.isSkippedMoment(),
65
- dismissed: notification.isDismissedMoment(),
66
- dismissedReason: notification.getDismissedReason()
67
- }
68
- });
69
- }
97
+ settled = true;
98
+ resolve(result);
99
+ };
100
+ const client = window.google.accounts.oauth2.initCodeClient({
101
+ client_id,
102
+ scope,
103
+ ux_mode,
104
+ redirect_uri,
105
+ state,
106
+ login_hint,
107
+ hd,
108
+ callback: (response) => settle(response.code ? { ok: true, response } : { ok: false, error: response }),
109
+ error_callback: (error) => settle({ ok: false, error })
70
110
  });
111
+ client.requestCode();
71
112
  });
72
113
  }
73
114
  export {
74
- prompt
115
+ prompt,
116
+ requestCode
75
117
  };
76
118
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/google-one-tap/index.ts"],"sourcesContent":["import type { CredentialResponse, GoogleAccounts, PromptMomentNotification } from './types';\n\ndeclare global {\n interface Window {\n google: {\n accounts: GoogleAccounts;\n };\n }\n}\n\nexport type Props = {\n /**\n * Opaque nonce string forwarded as-is to Google. Google echoes it back in\n * the ID Token's `nonce` claim per the OIDC spec; the caller is responsible\n * for any hashing/encoding required by its verifier.\n */\n nonce?: string;\n client_id: string;\n auto_select?: boolean;\n cancel_on_tap_outside?: boolean;\n use_fedcm_for_prompt?: boolean;\n};\n\nexport type PromptMoment = {\n skipped: boolean;\n dismissed: boolean;\n momentType: ReturnType<PromptMomentNotification['getMomentType']>;\n dismissedReason: ReturnType<PromptMomentNotification['getDismissedReason']>;\n};\n\nexport type PromptResult =\n | { authorized: true; credential: CredentialResponse }\n | { authorized: false; moment: PromptMoment };\n\nlet script: HTMLScriptElement | null = null;\n\nfunction loadScript(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (script) {\n if (window.google?.accounts?.id) {\n resolve();\n } else {\n script.addEventListener('load', () => resolve());\n script.addEventListener('error', () =>\n reject(new Error('Failed to load Google One Tap script'))\n );\n }\n return;\n }\n\n script = document.createElement('script');\n script.id = 'google-one-tap';\n script.async = true;\n script.defer = true;\n script.src = 'https://accounts.google.com/gsi/client';\n script.onload = () => resolve();\n script.onerror = () => reject(new Error('Failed to load Google One Tap script'));\n\n document.head.appendChild(script);\n });\n}\n\n/** debug: chrome://settings/content/federatedIdentityApi */\nexport async function prompt({\n nonce,\n client_id,\n auto_select = false,\n use_fedcm_for_prompt = true,\n cancel_on_tap_outside = false,\n}: Props): Promise<PromptResult> {\n await loadScript();\n\n return new Promise<PromptResult>((resolve) => {\n let settled = false;\n\n window.google.accounts.id.initialize({\n ux_mode: 'popup',\n context: 'signin',\n auto_select,\n nonce,\n client_id,\n use_fedcm_for_prompt,\n cancel_on_tap_outside,\n callback: (credential) => {\n if (settled) return;\n settled = true;\n resolve({ authorized: true, credential });\n },\n native_callback: (credential) => {\n if (settled) return;\n settled = true;\n resolve({ authorized: true, credential });\n },\n });\n\n window.google.accounts.id.prompt((notification) => {\n if (settled) return;\n\n if (\n notification.isSkippedMoment() ||\n (notification.isDismissedMoment() &&\n notification.getDismissedReason() !== 'credential_returned')\n ) {\n settled = true;\n resolve({\n authorized: false,\n moment: {\n momentType: notification.getMomentType(),\n skipped: notification.isSkippedMoment(),\n dismissed: notification.isDismissedMoment(),\n dismissedReason: notification.getDismissedReason(),\n },\n });\n }\n });\n });\n}\n"],"mappings":";AAkCA,IAAI,SAAmC;AAEvC,SAAS,aAA4B;AACnC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,QAAQ;AACV,UAAI,OAAO,QAAQ,UAAU,IAAI;AAC/B,gBAAQ;AAAA,MACV,OAAO;AACL,eAAO,iBAAiB,QAAQ,MAAM,QAAQ,CAAC;AAC/C,eAAO;AAAA,UAAiB;AAAA,UAAS,MAC/B,OAAO,IAAI,MAAM,sCAAsC,CAAC;AAAA,QAC1D;AAAA,MACF;AACA;AAAA,IACF;AAEA,aAAS,SAAS,cAAc,QAAQ;AACxC,WAAO,KAAK;AACZ,WAAO,QAAQ;AACf,WAAO,QAAQ;AACf,WAAO,MAAM;AACb,WAAO,SAAS,MAAM,QAAQ;AAC9B,WAAO,UAAU,MAAM,OAAO,IAAI,MAAM,sCAAsC,CAAC;AAE/E,aAAS,KAAK,YAAY,MAAM;AAAA,EAClC,CAAC;AACH;AAGA,eAAsB,OAAO;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,uBAAuB;AAAA,EACvB,wBAAwB;AAC1B,GAAiC;AAC/B,QAAM,WAAW;AAEjB,SAAO,IAAI,QAAsB,CAAC,YAAY;AAC5C,QAAI,UAAU;AAEd,WAAO,OAAO,SAAS,GAAG,WAAW;AAAA,MACnC,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,eAAe;AACxB,YAAI,QAAS;AACb,kBAAU;AACV,gBAAQ,EAAE,YAAY,MAAM,WAAW,CAAC;AAAA,MAC1C;AAAA,MACA,iBAAiB,CAAC,eAAe;AAC/B,YAAI,QAAS;AACb,kBAAU;AACV,gBAAQ,EAAE,YAAY,MAAM,WAAW,CAAC;AAAA,MAC1C;AAAA,IACF,CAAC;AAED,WAAO,OAAO,SAAS,GAAG,OAAO,CAAC,iBAAiB;AACjD,UAAI,QAAS;AAEb,UACE,aAAa,gBAAgB,KAC5B,aAAa,kBAAkB,KAC9B,aAAa,mBAAmB,MAAM,uBACxC;AACA,kBAAU;AACV,gBAAQ;AAAA,UACN,YAAY;AAAA,UACZ,QAAQ;AAAA,YACN,YAAY,aAAa,cAAc;AAAA,YACvC,SAAS,aAAa,gBAAgB;AAAA,YACtC,WAAW,aAAa,kBAAkB;AAAA,YAC1C,iBAAiB,aAAa,mBAAmB;AAAA,UACnD;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/google-one-tap/index.ts"],"sourcesContent":["import type {\n ClientConfigError,\n CodeResponse,\n CredentialResponse,\n GoogleAccounts,\n PromptMomentNotification,\n} from './types';\n\ndeclare global {\n interface Window {\n google: {\n accounts: GoogleAccounts;\n };\n }\n}\n\nexport type Props = {\n /**\n * Opaque nonce string forwarded as-is to Google. Google echoes it back in\n * the ID Token's `nonce` claim per the OIDC spec; the caller is responsible\n * for any hashing/encoding required by its verifier.\n */\n nonce?: string;\n client_id: string;\n auto_select?: boolean;\n cancel_on_tap_outside?: boolean;\n use_fedcm_for_prompt?: boolean;\n};\n\nexport type PromptMoment = {\n skipped: boolean;\n dismissed: boolean;\n momentType: ReturnType<PromptMomentNotification['getMomentType']>;\n dismissedReason: ReturnType<PromptMomentNotification['getDismissedReason']>;\n};\n\n/**\n * Why prompt() could not run One Tap, so the caller can decide to fall back\n * to requestCode().\n * - `fedcm_unsupported`: browser has no FedCM (Safari/Firefox/pre-117 Chrome).\n * - `script_load_failed`: the GSI script failed to load (network/CSP).\n * - `prompt_error`: GIS threw synchronously while initializing/prompting.\n */\nexport type UnsupportedReason = 'fedcm_unsupported' | 'script_load_failed' | 'prompt_error';\n\nexport type PromptResult =\n | { authorized: true; credential: CredentialResponse }\n | { authorized: false; moment: PromptMoment }\n | { authorized: false; unsupported: true; reason: UnsupportedReason };\n\nlet script: HTMLScriptElement | null = null;\n\nfunction loadScript(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (script) {\n if (window.google?.accounts?.id) {\n resolve();\n } else {\n script.addEventListener('load', () => resolve());\n script.addEventListener('error', () =>\n reject(new Error('Failed to load Google One Tap script'))\n );\n }\n return;\n }\n\n script = document.createElement('script');\n script.id = 'google-one-tap';\n script.async = true;\n script.defer = true;\n script.src = 'https://accounts.google.com/gsi/client';\n script.onload = () => resolve();\n script.onerror = () => reject(new Error('Failed to load Google One Tap script'));\n\n document.head.appendChild(script);\n });\n}\n\n/** FedCM feature detection — `IdentityCredential` is absent on Safari, Firefox and pre-117 Chrome. */\nfunction isFedcmSupported(): boolean {\n return typeof window !== 'undefined' && 'IdentityCredential' in window;\n}\n\n/** debug: chrome://settings/content/federatedIdentityApi */\nexport async function prompt({\n nonce,\n client_id,\n auto_select = false,\n use_fedcm_for_prompt = true,\n cancel_on_tap_outside = false,\n}: Props): Promise<PromptResult> {\n // FedCM is mandatory for One Tap on capable browsers (GIS completed the\n // migration in early 2025) and there is no viable legacy fallback anymore.\n // When the caller wants FedCM but the browser lacks it, bail early instead\n // of letting GIS throw an async `NotSupportedError: Missing request type`\n // from navigator.credentials.get. Callers should fall back to requestCode().\n if (use_fedcm_for_prompt !== false && !isFedcmSupported()) {\n return { authorized: false, unsupported: true, reason: 'fedcm_unsupported' };\n }\n\n try {\n await loadScript();\n } catch {\n return { authorized: false, unsupported: true, reason: 'script_load_failed' };\n }\n\n return new Promise<PromptResult>((resolve) => {\n let settled = false;\n const settle = (result: PromptResult) => {\n if (settled) return;\n settled = true;\n resolve(result);\n };\n\n try {\n window.google.accounts.id.initialize({\n ux_mode: 'popup',\n context: 'signin',\n auto_select,\n nonce,\n client_id,\n use_fedcm_for_prompt,\n cancel_on_tap_outside,\n callback: (credential) => settle({ authorized: true, credential }),\n native_callback: (credential) => settle({ authorized: true, credential }),\n });\n\n window.google.accounts.id.prompt((notification) => {\n if (settled) return;\n\n if (\n notification.isSkippedMoment() ||\n (notification.isDismissedMoment() &&\n notification.getDismissedReason() !== 'credential_returned')\n ) {\n settle({\n authorized: false,\n moment: {\n momentType: notification.getMomentType(),\n skipped: notification.isSkippedMoment(),\n dismissed: notification.isDismissedMoment(),\n dismissedReason: notification.getDismissedReason(),\n },\n });\n }\n });\n } catch {\n settle({ authorized: false, unsupported: true, reason: 'prompt_error' });\n }\n });\n}\n\nexport type CodeProps = {\n client_id: string;\n /**\n * Space-delimited OAuth scopes. Defaults to OpenID Connect scopes so the\n * backend can exchange the code for an ID token, mirroring One Tap's intent.\n */\n scope?: string;\n /** 'popup' resolves the returned promise; 'redirect' navigates away and never resolves. */\n ux_mode?: 'popup' | 'redirect';\n /** Required when ux_mode is 'redirect'; for 'popup' Google uses postMessage. */\n redirect_uri?: string;\n state?: string;\n login_hint?: string;\n hd?: string;\n};\n\nexport type CodeResult =\n | { ok: true; response: CodeResponse }\n | { ok: false; error: ClientConfigError | CodeResponse };\n\n/**\n * OAuth 2.0 authorization-code fallback for environments where One Tap / FedCM\n * is unavailable (i.e. prompt() resolved with `unsupported: true`). It reuses\n * the same GSI script, so there is no extra dependency.\n *\n * Unlike One Tap it returns an authorization `code` for the backend to\n * exchange, not an ID token, and it must be triggered by a user gesture or the\n * popup may be blocked.\n */\nexport async function requestCode({\n client_id,\n scope = 'openid email profile',\n ux_mode = 'popup',\n redirect_uri,\n state,\n login_hint,\n hd,\n}: CodeProps): Promise<CodeResult> {\n await loadScript();\n\n return new Promise<CodeResult>((resolve) => {\n let settled = false;\n const settle = (result: CodeResult) => {\n if (settled) return;\n settled = true;\n resolve(result);\n };\n\n const client = window.google.accounts.oauth2.initCodeClient({\n client_id,\n scope,\n ux_mode,\n redirect_uri,\n state,\n login_hint,\n hd,\n callback: (response) =>\n settle(response.code ? { ok: true, response } : { ok: false, error: response }),\n error_callback: (error) => settle({ ok: false, error }),\n });\n\n client.requestCode();\n });\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/google-one-tap/types.ts"],"sourcesContent":["interface ClientConfigError extends Error {\n message: string;\n stack?: string;\n type: 'unknown' | 'popup_closed' | 'popup_failed_to_open';\n}\n\ninterface OverridableTokenClientConfig {\n scope?: string;\n include_granted_scopes?: boolean;\n prompt?: string;\n enable_granular_consent?: boolean;\n enable_serial_consent?: boolean;\n login_hint?: string;\n hint?: string;\n state?: string;\n}\n\ninterface TokenClient {\n requestAccessToken: (overrideConfig?: OverridableTokenClientConfig) => void;\n}\n\ninterface CodeClient {\n requestCode: () => void;\n}\n\ninterface TokenResponse {\n access_token: string;\n expires_in: string;\n hd: string;\n prompt: string;\n token_type: string;\n scope: string;\n state: string;\n error: string;\n error_description: string;\n error_uri: string;\n}\n\ninterface CodeResponse {\n code: string;\n scope: string;\n state: string;\n error: string;\n error_description: string;\n error_uri: string;\n}\n\ninterface TokenClientConfig {\n client_id: string;\n scope: string;\n include_granted_scopes?: boolean;\n prompt?: '' | 'none' | 'consent' | 'select_account';\n enable_granular_consent?: boolean;\n enable_serial_consent?: boolean;\n login_hint?: string;\n hint?: string;\n hd?: string;\n hosted_domain?: string;\n state?: string;\n callback: (tokenResponse: TokenResponse) => void;\n error_callback?: (error: ClientConfigError) => void;\n}\n\ninterface CodeClientConfig {\n client_id: string;\n scope: string;\n include_granted_scopes?: boolean;\n redirect_uri?: string;\n state?: string;\n enable_granular_consent?: boolean;\n enable_serial_consent?: boolean;\n login_hint?: string;\n hint?: string;\n hd?: string;\n hosted_domain?: string;\n ux_mode?: 'popup' | 'redirect';\n select_account?: boolean;\n callback?: (response: CodeResponse) => void;\n error_callback?: (error: ClientConfigError) => void;\n}\n\ninterface RevocationResponse {\n successful: boolean;\n error?: string;\n}\n\ninterface Credential {\n id: string;\n password: string;\n}\n\nexport interface CredentialResponse {\n credential: string;\n select_by: string;\n}\n\nexport interface IdConfiguration {\n client_id: string;\n auto_select?: boolean;\n login_uri?: string;\n cancel_on_tap_outside?: boolean;\n prompt_parent_id?: string;\n nonce?: string;\n context?: 'signin' | 'signup' | 'use';\n state_cookie_domain?: string;\n ux_mode?: 'popup' | 'redirect';\n allowed_parent_origin?: string | string[];\n itp_support?: boolean;\n login_hint?: string;\n hd?: string;\n use_fedcm_for_prompt?: boolean;\n callback?: (response: CredentialResponse) => void;\n native_callback?: (response: CredentialResponse) => void;\n intermediate_iframe_close_callback?: () => void;\n}\n/**\n * ref: https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc&utm_source=devtools&utm_campaign=stable#display_moment\n */\nexport interface PromptMomentNotification {\n /** @deprecated */\n isDisplayMoment(): boolean;\n\n /** @deprecated */\n isDisplayed(): boolean;\n\n /** @deprecated */\n isNotDisplayed(): boolean;\n\n /** @deprecated */\n getNotDisplayedReason():\n | 'browser_not_supported'\n | 'invalid_client'\n | 'missing_client_id'\n | 'opt_out_or_no_session'\n | 'secure_http_required'\n | 'suppressed_by_user'\n | 'unregistered_origin'\n | 'unknown_reason';\n\n /** @deprecated */\n getSkippedReason(): 'auto_cancel' | 'user_cancel' | 'tap_outside' | 'issuing_failed';\n\n isSkippedMoment(): boolean;\n isDismissedMoment(): boolean;\n getDismissedReason(): 'credential_returned' | 'cancel_called' | 'flow_restarted';\n getMomentType(): 'display' | 'skipped' | 'dismissed';\n}\n\ninterface GsiButtonConfiguration {\n type: 'standard' | 'icon';\n theme?: 'outline' | 'filled_blue' | 'filled_black';\n size?: 'small' | 'medium' | 'large';\n text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';\n shape?: 'rectangular' | 'pill' | 'circle' | 'square';\n logo_alignment?: 'left' | 'center';\n width?: number;\n locale?: string;\n state?: string;\n click_listener?: () => void;\n}\n\nexport interface GoogleAccounts {\n oauth2: {\n initCodeClient(config: CodeClientConfig): CodeClient;\n initTokenClient(config: TokenClientConfig): TokenClient;\n hasGrantedAllScopes(\n tokenResponse: TokenResponse,\n firstScope: string,\n ...restScopes: string[]\n ): boolean;\n hasGrantedAnyScope(\n tokenResponse: TokenResponse,\n firstScope: string,\n ...restScopes: string[]\n ): boolean;\n revoke(accessToken: string, done: () => void): void;\n };\n id: {\n initialize(idConfig: IdConfiguration): void;\n prompt(momentListener?: (promptMomentNotification: PromptMomentNotification) => void): void;\n renderButton(parent: HTMLElement, options: GsiButtonConfiguration): void;\n disableAutoSelect(): void;\n storeCredential(credential: Credential, callback?: () => void): void;\n cancel(): void;\n revoke(hint: string, callback?: (response: RevocationResponse) => void): void;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../../src/google-one-tap/types.ts"],"sourcesContent":["export interface ClientConfigError extends Error {\n message: string;\n stack?: string;\n type: 'unknown' | 'popup_closed' | 'popup_failed_to_open';\n}\n\ninterface OverridableTokenClientConfig {\n scope?: string;\n include_granted_scopes?: boolean;\n prompt?: string;\n enable_granular_consent?: boolean;\n enable_serial_consent?: boolean;\n login_hint?: string;\n hint?: string;\n state?: string;\n}\n\ninterface TokenClient {\n requestAccessToken: (overrideConfig?: OverridableTokenClientConfig) => void;\n}\n\ninterface CodeClient {\n requestCode: () => void;\n}\n\ninterface TokenResponse {\n access_token: string;\n expires_in: string;\n hd: string;\n prompt: string;\n token_type: string;\n scope: string;\n state: string;\n error: string;\n error_description: string;\n error_uri: string;\n}\n\nexport interface CodeResponse {\n code: string;\n scope: string;\n state: string;\n error: string;\n error_description: string;\n error_uri: string;\n}\n\ninterface TokenClientConfig {\n client_id: string;\n scope: string;\n include_granted_scopes?: boolean;\n prompt?: '' | 'none' | 'consent' | 'select_account';\n enable_granular_consent?: boolean;\n enable_serial_consent?: boolean;\n login_hint?: string;\n hint?: string;\n hd?: string;\n hosted_domain?: string;\n state?: string;\n callback: (tokenResponse: TokenResponse) => void;\n error_callback?: (error: ClientConfigError) => void;\n}\n\ninterface CodeClientConfig {\n client_id: string;\n scope: string;\n include_granted_scopes?: boolean;\n redirect_uri?: string;\n state?: string;\n enable_granular_consent?: boolean;\n enable_serial_consent?: boolean;\n login_hint?: string;\n hint?: string;\n hd?: string;\n hosted_domain?: string;\n ux_mode?: 'popup' | 'redirect';\n select_account?: boolean;\n callback?: (response: CodeResponse) => void;\n error_callback?: (error: ClientConfigError) => void;\n}\n\ninterface RevocationResponse {\n successful: boolean;\n error?: string;\n}\n\ninterface Credential {\n id: string;\n password: string;\n}\n\nexport interface CredentialResponse {\n credential: string;\n select_by: string;\n}\n\nexport interface IdConfiguration {\n client_id: string;\n auto_select?: boolean;\n login_uri?: string;\n cancel_on_tap_outside?: boolean;\n prompt_parent_id?: string;\n nonce?: string;\n context?: 'signin' | 'signup' | 'use';\n state_cookie_domain?: string;\n ux_mode?: 'popup' | 'redirect';\n allowed_parent_origin?: string | string[];\n itp_support?: boolean;\n login_hint?: string;\n hd?: string;\n use_fedcm_for_prompt?: boolean;\n callback?: (response: CredentialResponse) => void;\n native_callback?: (response: CredentialResponse) => void;\n intermediate_iframe_close_callback?: () => void;\n}\n/**\n * ref: https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc&utm_source=devtools&utm_campaign=stable#display_moment\n */\nexport interface PromptMomentNotification {\n /** @deprecated */\n isDisplayMoment(): boolean;\n\n /** @deprecated */\n isDisplayed(): boolean;\n\n /** @deprecated */\n isNotDisplayed(): boolean;\n\n /** @deprecated */\n getNotDisplayedReason():\n | 'browser_not_supported'\n | 'invalid_client'\n | 'missing_client_id'\n | 'opt_out_or_no_session'\n | 'secure_http_required'\n | 'suppressed_by_user'\n | 'unregistered_origin'\n | 'unknown_reason';\n\n /** @deprecated */\n getSkippedReason(): 'auto_cancel' | 'user_cancel' | 'tap_outside' | 'issuing_failed';\n\n isSkippedMoment(): boolean;\n isDismissedMoment(): boolean;\n getDismissedReason(): 'credential_returned' | 'cancel_called' | 'flow_restarted';\n getMomentType(): 'display' | 'skipped' | 'dismissed';\n}\n\ninterface GsiButtonConfiguration {\n type: 'standard' | 'icon';\n theme?: 'outline' | 'filled_blue' | 'filled_black';\n size?: 'small' | 'medium' | 'large';\n text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';\n shape?: 'rectangular' | 'pill' | 'circle' | 'square';\n logo_alignment?: 'left' | 'center';\n width?: number;\n locale?: string;\n state?: string;\n click_listener?: () => void;\n}\n\nexport interface GoogleAccounts {\n oauth2: {\n initCodeClient(config: CodeClientConfig): CodeClient;\n initTokenClient(config: TokenClientConfig): TokenClient;\n hasGrantedAllScopes(\n tokenResponse: TokenResponse,\n firstScope: string,\n ...restScopes: string[]\n ): boolean;\n hasGrantedAnyScope(\n tokenResponse: TokenResponse,\n firstScope: string,\n ...restScopes: string[]\n ): boolean;\n revoke(accessToken: string, done: () => void): void;\n };\n id: {\n initialize(idConfig: IdConfiguration): void;\n prompt(momentListener?: (promptMomentNotification: PromptMomentNotification) => void): void;\n renderButton(parent: HTMLElement, options: GsiButtonConfiguration): void;\n disableAutoSelect(): void;\n storeCredential(credential: Credential, callback?: () => void): void;\n cancel(): void;\n revoke(hint: string, callback?: (response: RevocationResponse) => void): void;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -152,4 +152,4 @@ interface GoogleAccounts {
152
152
  };
153
153
  }
154
154
 
155
- export type { CredentialResponse, GoogleAccounts, IdConfiguration, PromptMomentNotification };
155
+ export type { ClientConfigError, CodeResponse, CredentialResponse, GoogleAccounts, IdConfiguration, PromptMomentNotification };
@@ -152,4 +152,4 @@ interface GoogleAccounts {
152
152
  };
153
153
  }
154
154
 
155
- export type { CredentialResponse, GoogleAccounts, IdConfiguration, PromptMomentNotification };
155
+ export type { ClientConfigError, CodeResponse, CredentialResponse, GoogleAccounts, IdConfiguration, PromptMomentNotification };
@@ -41,11 +41,11 @@ async function forwardToGoogleTagGateway(request, gaId) {
41
41
  headers.set("x-forwarded-region", region);
42
42
  }
43
43
  const hasBody = request.method !== "GET" && request.method !== "HEAD";
44
+ const body = hasBody ? await request.arrayBuffer() : void 0;
44
45
  const response = await fetch(target, {
45
46
  method: request.method,
46
47
  headers,
47
- body: hasBody ? request.body : void 0,
48
- ...hasBody && { duplex: "half" }
48
+ body
49
49
  });
50
50
  const responseHeaders = new Headers(response.headers);
51
51
  responseHeaders.delete("content-encoding");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n headers.set('host', GATEWAY_HOST);\n\n // Forward cookies\n const cookie = request.headers.get('cookie');\n if (cookie) headers.set('cookie', cookie);\n\n // Convert Vercel geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region } = getGeolocation(request);\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n } else if (region) {\n headers.set('x-forwarded-region', region);\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n const response = await fetch(target, {\n method: request.method,\n headers,\n body: hasBody ? request.body : undefined,\n ...(hasBody && { duplex: 'half' as const }),\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA+B;AAO/B,eAAsB,0BAA0B,SAAkB,MAAc;AAC9E,QAAM,eAAe,GAAG,IAAI;AAC5B,QAAM,EAAE,UAAU,OAAO,IAAI,IAAI,IAAI,QAAQ,GAAG;AAEhD,QAAM,SAAS,WAAW,YAAY,GAAG,QAAQ,GAAG,MAAM;AAE1D,QAAM,UAAU,IAAI,QAAQ;AAC5B,UAAQ,IAAI,QAAQ,YAAY;AAGhC,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,MAAI,OAAQ,SAAQ,IAAI,UAAU,MAAM;AAIxC,QAAM,EAAE,SAAS,OAAO,QAAI,mCAAe,OAAO;AAElD,MAAI,WAAW,QAAQ;AACrB,YAAQ,IAAI,6BAA6B,GAAG,OAAO,IAAI,MAAM,EAAE;AAAA,EACjE,WAAW,SAAS;AAClB,YAAQ,IAAI,uBAAuB,OAAO;AAAA,EAC5C,WAAW,QAAQ;AACjB,YAAQ,IAAI,sBAAsB,MAAM;AAAA,EAC1C;AAEA,QAAM,UAAU,QAAQ,WAAW,SAAS,QAAQ,WAAW;AAC/D,QAAM,WAAW,MAAM,MAAM,QAAQ;AAAA,IACnC,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA,MAAM,UAAU,QAAQ,OAAO;AAAA,IAC/B,GAAI,WAAW,EAAE,QAAQ,OAAgB;AAAA,EAC3C,CAAC;AAID,QAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;AACpD,kBAAgB,OAAO,kBAAkB;AACzC,kBAAgB,OAAO,gBAAgB;AAEvC,SAAO,IAAI,SAAS,SAAS,MAAM,EAAE,QAAQ,SAAS,QAAQ,SAAS,gBAAgB,CAAC;AAC1F;","names":[]}
1
+ {"version":3,"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n headers.set('host', GATEWAY_HOST);\n\n // Forward cookies\n const cookie = request.headers.get('cookie');\n if (cookie) headers.set('cookie', cookie);\n\n // Convert Vercel geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region } = getGeolocation(request);\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n } else if (region) {\n headers.set('x-forwarded-region', region);\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n\n // Buffer the body instead of streaming it. The conversion endpoint\n // (g/measurement/conversion) answers POSTs with a 302 to www.google.com, and we want\n // to follow that hop on the server so the browser never sees a Google domain (the whole\n // point of the first-party gateway). Following a redirect requires re-issuing the\n // request, which undici cannot do with a one-shot `request.body` stream — it throws\n // \"fetch failed\", surfacing as a 500. A buffered body is replayable, so the redirect\n // is followed transparently here.\n const body = hasBody ? await request.arrayBuffer() : undefined;\n\n const response = await fetch(target, {\n method: request.method,\n headers,\n body,\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA+B;AAO/B,eAAsB,0BAA0B,SAAkB,MAAc;AAC9E,QAAM,eAAe,GAAG,IAAI;AAC5B,QAAM,EAAE,UAAU,OAAO,IAAI,IAAI,IAAI,QAAQ,GAAG;AAEhD,QAAM,SAAS,WAAW,YAAY,GAAG,QAAQ,GAAG,MAAM;AAE1D,QAAM,UAAU,IAAI,QAAQ;AAC5B,UAAQ,IAAI,QAAQ,YAAY;AAGhC,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,MAAI,OAAQ,SAAQ,IAAI,UAAU,MAAM;AAIxC,QAAM,EAAE,SAAS,OAAO,QAAI,mCAAe,OAAO;AAElD,MAAI,WAAW,QAAQ;AACrB,YAAQ,IAAI,6BAA6B,GAAG,OAAO,IAAI,MAAM,EAAE;AAAA,EACjE,WAAW,SAAS;AAClB,YAAQ,IAAI,uBAAuB,OAAO;AAAA,EAC5C,WAAW,QAAQ;AACjB,YAAQ,IAAI,sBAAsB,MAAM;AAAA,EAC1C;AAEA,QAAM,UAAU,QAAQ,WAAW,SAAS,QAAQ,WAAW;AAS/D,QAAM,OAAO,UAAU,MAAM,QAAQ,YAAY,IAAI;AAErD,QAAM,WAAW,MAAM,MAAM,QAAQ;AAAA,IACnC,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA;AAAA,EACF,CAAC;AAID,QAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;AACpD,kBAAgB,OAAO,kBAAkB;AACzC,kBAAgB,OAAO,gBAAgB;AAEvC,SAAO,IAAI,SAAS,SAAS,MAAM,EAAE,QAAQ,SAAS,QAAQ,SAAS,gBAAgB,CAAC;AAC1F;","names":[]}
@@ -17,11 +17,11 @@ async function forwardToGoogleTagGateway(request, gaId) {
17
17
  headers.set("x-forwarded-region", region);
18
18
  }
19
19
  const hasBody = request.method !== "GET" && request.method !== "HEAD";
20
+ const body = hasBody ? await request.arrayBuffer() : void 0;
20
21
  const response = await fetch(target, {
21
22
  method: request.method,
22
23
  headers,
23
- body: hasBody ? request.body : void 0,
24
- ...hasBody && { duplex: "half" }
24
+ body
25
25
  });
26
26
  const responseHeaders = new Headers(response.headers);
27
27
  responseHeaders.delete("content-encoding");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n headers.set('host', GATEWAY_HOST);\n\n // Forward cookies\n const cookie = request.headers.get('cookie');\n if (cookie) headers.set('cookie', cookie);\n\n // Convert Vercel geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region } = getGeolocation(request);\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n } else if (region) {\n headers.set('x-forwarded-region', region);\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n const response = await fetch(target, {\n method: request.method,\n headers,\n body: hasBody ? request.body : undefined,\n ...(hasBody && { duplex: 'half' as const }),\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";AAAA,SAAS,sBAAsB;AAO/B,eAAsB,0BAA0B,SAAkB,MAAc;AAC9E,QAAM,eAAe,GAAG,IAAI;AAC5B,QAAM,EAAE,UAAU,OAAO,IAAI,IAAI,IAAI,QAAQ,GAAG;AAEhD,QAAM,SAAS,WAAW,YAAY,GAAG,QAAQ,GAAG,MAAM;AAE1D,QAAM,UAAU,IAAI,QAAQ;AAC5B,UAAQ,IAAI,QAAQ,YAAY;AAGhC,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,MAAI,OAAQ,SAAQ,IAAI,UAAU,MAAM;AAIxC,QAAM,EAAE,SAAS,OAAO,IAAI,eAAe,OAAO;AAElD,MAAI,WAAW,QAAQ;AACrB,YAAQ,IAAI,6BAA6B,GAAG,OAAO,IAAI,MAAM,EAAE;AAAA,EACjE,WAAW,SAAS;AAClB,YAAQ,IAAI,uBAAuB,OAAO;AAAA,EAC5C,WAAW,QAAQ;AACjB,YAAQ,IAAI,sBAAsB,MAAM;AAAA,EAC1C;AAEA,QAAM,UAAU,QAAQ,WAAW,SAAS,QAAQ,WAAW;AAC/D,QAAM,WAAW,MAAM,MAAM,QAAQ;AAAA,IACnC,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA,MAAM,UAAU,QAAQ,OAAO;AAAA,IAC/B,GAAI,WAAW,EAAE,QAAQ,OAAgB;AAAA,EAC3C,CAAC;AAID,QAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;AACpD,kBAAgB,OAAO,kBAAkB;AACzC,kBAAgB,OAAO,gBAAgB;AAEvC,SAAO,IAAI,SAAS,SAAS,MAAM,EAAE,QAAQ,SAAS,QAAQ,SAAS,gBAAgB,CAAC;AAC1F;","names":[]}
1
+ {"version":3,"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n headers.set('host', GATEWAY_HOST);\n\n // Forward cookies\n const cookie = request.headers.get('cookie');\n if (cookie) headers.set('cookie', cookie);\n\n // Convert Vercel geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region } = getGeolocation(request);\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n } else if (region) {\n headers.set('x-forwarded-region', region);\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n\n // Buffer the body instead of streaming it. The conversion endpoint\n // (g/measurement/conversion) answers POSTs with a 302 to www.google.com, and we want\n // to follow that hop on the server so the browser never sees a Google domain (the whole\n // point of the first-party gateway). Following a redirect requires re-issuing the\n // request, which undici cannot do with a one-shot `request.body` stream — it throws\n // \"fetch failed\", surfacing as a 500. A buffered body is replayable, so the redirect\n // is followed transparently here.\n const body = hasBody ? await request.arrayBuffer() : undefined;\n\n const response = await fetch(target, {\n method: request.method,\n headers,\n body,\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";AAAA,SAAS,sBAAsB;AAO/B,eAAsB,0BAA0B,SAAkB,MAAc;AAC9E,QAAM,eAAe,GAAG,IAAI;AAC5B,QAAM,EAAE,UAAU,OAAO,IAAI,IAAI,IAAI,QAAQ,GAAG;AAEhD,QAAM,SAAS,WAAW,YAAY,GAAG,QAAQ,GAAG,MAAM;AAE1D,QAAM,UAAU,IAAI,QAAQ;AAC5B,UAAQ,IAAI,QAAQ,YAAY;AAGhC,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,MAAI,OAAQ,SAAQ,IAAI,UAAU,MAAM;AAIxC,QAAM,EAAE,SAAS,OAAO,IAAI,eAAe,OAAO;AAElD,MAAI,WAAW,QAAQ;AACrB,YAAQ,IAAI,6BAA6B,GAAG,OAAO,IAAI,MAAM,EAAE;AAAA,EACjE,WAAW,SAAS;AAClB,YAAQ,IAAI,uBAAuB,OAAO;AAAA,EAC5C,WAAW,QAAQ;AACjB,YAAQ,IAAI,sBAAsB,MAAM;AAAA,EAC1C;AAEA,QAAM,UAAU,QAAQ,WAAW,SAAS,QAAQ,WAAW;AAS/D,QAAM,OAAO,UAAU,MAAM,QAAQ,YAAY,IAAI;AAErD,QAAM,WAAW,MAAM,MAAM,QAAQ;AAAA,IACnC,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA;AAAA,EACF,CAAC;AAID,QAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;AACpD,kBAAgB,OAAO,kBAAkB;AACzC,kBAAgB,OAAO,gBAAgB;AAEvC,SAAO,IAAI,SAAS,SAAS,MAAM,EAAE,QAAQ,SAAS,QAAQ,SAAS,gBAAgB,CAAC;AAC1F;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/http",
3
- "version": "2.10.4",
3
+ "version": "2.11.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "vitest": "^4.1.5",
42
42
  "zod": "^4.4.3",
43
- "@shware/utils": "^1.4.1"
43
+ "@shware/utils": "^1.4.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^25",