@volter/twin-googleoauth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +219 -0
  2. package/client/googleoauth-consent.css +207 -0
  3. package/client/googleoauth-consent.tsx +286 -0
  4. package/dist/client/googleoauth-consent.bundle.js +237 -0
  5. package/dist/client/googleoauth-consent.css +207 -0
  6. package/dist/client/googleoauth-consent.d.ts +88 -0
  7. package/dist/client/googleoauth-consent.js +94 -0
  8. package/dist/client/googleoauth-consent.tsx +286 -0
  9. package/dist/src/cli.d.ts +2 -0
  10. package/dist/src/cli.js +42 -0
  11. package/dist/src/googleoauth-autherror.d.ts +25 -0
  12. package/dist/src/googleoauth-autherror.js +144 -0
  13. package/dist/src/googleoauth-budget.d.ts +48 -0
  14. package/dist/src/googleoauth-budget.js +121 -0
  15. package/dist/src/googleoauth-capabilities.d.ts +3 -0
  16. package/dist/src/googleoauth-capabilities.js +1651 -0
  17. package/dist/src/googleoauth-conformance.d.ts +10 -0
  18. package/dist/src/googleoauth-conformance.js +426 -0
  19. package/dist/src/googleoauth-connector.d.ts +70 -0
  20. package/dist/src/googleoauth-connector.js +244 -0
  21. package/dist/src/googleoauth-consent-client.gen.d.ts +2 -0
  22. package/dist/src/googleoauth-consent-client.gen.js +10 -0
  23. package/dist/src/googleoauth-consent-ui.d.ts +25 -0
  24. package/dist/src/googleoauth-consent-ui.js +102 -0
  25. package/dist/src/googleoauth-jwt.d.ts +78 -0
  26. package/dist/src/googleoauth-jwt.js +183 -0
  27. package/dist/src/googleoauth-scopes.d.ts +36 -0
  28. package/dist/src/googleoauth-scopes.js +92 -0
  29. package/dist/src/googleoauth-server.d.ts +34 -0
  30. package/dist/src/googleoauth-server.js +89 -0
  31. package/dist/src/googleoauth-store.d.ts +78 -0
  32. package/dist/src/googleoauth-store.js +313 -0
  33. package/dist/src/googleoauth-twin.d.ts +53 -0
  34. package/dist/src/googleoauth-twin.js +1050 -0
  35. package/dist/src/index.d.ts +16 -0
  36. package/dist/src/index.js +102 -0
  37. package/package.json +75 -0
  38. package/src/cli.ts +41 -0
  39. package/src/googleoauth-autherror.ts +150 -0
  40. package/src/googleoauth-budget.ts +147 -0
  41. package/src/googleoauth-capabilities.ts +1775 -0
  42. package/src/googleoauth-conformance.ts +472 -0
  43. package/src/googleoauth-connector.ts +266 -0
  44. package/src/googleoauth-consent-client.gen.ts +10 -0
  45. package/src/googleoauth-consent-ui.ts +124 -0
  46. package/src/googleoauth-journey.uitest.ts +296 -0
  47. package/src/googleoauth-jwt.ts +207 -0
  48. package/src/googleoauth-scopes.ts +109 -0
  49. package/src/googleoauth-server.ts +101 -0
  50. package/src/googleoauth-store.ts +359 -0
  51. package/src/googleoauth-twin.ts +1207 -0
  52. package/src/index.ts +175 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
3
+ // world-googleoauth CLI: serve the Google OAuth 2.0 / OIDC twin, or run conformance.
4
+ //
5
+ // `serve` and `mirror` start the SAME server, because the consent screen is served by the twin at
6
+ // the vendor's own path — there is no second renderer to start. `mirror` additionally prints a
7
+ // ready-to-open authorization URL, which is the thing an operator actually wants when they ask to
8
+ // "see the UI".
9
+ import { hasFlag, optionValue } from '@volter/world-core/args';
10
+ import { createGoogleOAuthTwinServer } from "./googleoauth-server.js";
11
+ import { DEFAULT_CLIENT_ID, defaultRedirectUris } from "./googleoauth-store.js";
12
+ const [cmd, ...rest] = process.argv.slice(2);
13
+ const port = Number(optionValue(rest, '--port', '0')) || undefined;
14
+ const root = optionValue(rest, '--root') || undefined;
15
+ const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
16
+ if (cmd === 'serve' || cmd === 'mirror') {
17
+ const s = await createGoogleOAuthTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
18
+ const origin = `http://127.0.0.1:${s.port}`;
19
+ process.stdout.write(`googleoauth twin (OAuth 2.0 / OIDC)${readOnly ? ' [read-only]' : ''} at ${origin}\n`);
20
+ if (cmd === 'mirror') {
21
+ const url = new URL(`${origin}/o/oauth2/v2/auth`);
22
+ url.searchParams.set('client_id', DEFAULT_CLIENT_ID);
23
+ url.searchParams.set('redirect_uri', defaultRedirectUris(origin)[0]);
24
+ url.searchParams.set('response_type', 'code');
25
+ url.searchParams.set('scope', 'openid email profile https://www.googleapis.com/auth/calendar.readonly');
26
+ url.searchParams.set('state', 'twin-demo-state');
27
+ url.searchParams.set('access_type', 'offline');
28
+ process.stdout.write(`consent screen: ${url.toString()}\n`);
29
+ }
30
+ await keepProcessAlive();
31
+ }
32
+ else if (cmd === 'conformance') {
33
+ // dev-only; lazy so the bin runs without @volter/world-tooling in the runtime graph
34
+ const { checkGoogleOAuthConformance } = await import("./googleoauth-conformance.js");
35
+ const report = await checkGoogleOAuthConformance({ ...(root ? { root } : {}) });
36
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
37
+ if (!report.ok)
38
+ process.exitCode = 1;
39
+ }
40
+ else {
41
+ process.stdout.write('Usage: world-googleoauth serve|mirror|conformance [--port N] [--root DIR] [--read-only]\n');
42
+ }
@@ -0,0 +1,25 @@
1
+ export declare const ERROR_PAGE_PATH = "/signin/oauth/error";
2
+ /** Google puts this on every OAuth error-page URL; a real capture always carries it. */
3
+ export declare const FLOW_NAME = "GeneralOAuthFlow";
4
+ export type AuthError = {
5
+ /** The OAuth error code (protobuf field 1). */
6
+ code: string;
7
+ /** The human-readable message (field 2). */
8
+ message: string;
9
+ /** The documentation URL Google links (field 3). */
10
+ docUrl?: string;
11
+ /** The HTTP status the page NAMES in its "Error <status>: <code>" line (field 4). */
12
+ status: number;
13
+ /** The offending parameter, echoed back (field 5). */
14
+ param?: string;
15
+ };
16
+ /** Encode an AuthError as the base64url protobuf Google puts in `?authError=`. */
17
+ export declare function encodeAuthError(e: AuthError): string;
18
+ /** Decode a `?authError=` payload back into its fields. Returns null on anything unparseable. */
19
+ export declare function decodeAuthError(encoded: string): AuthError | null;
20
+ /** The full error-page URL an authorization request is bounced to. */
21
+ export declare function authErrorRedirect(origin: string, e: AuthError, clientId?: string): string;
22
+ /** The two headline strings Google's error pages use, chosen by error code (captured verbatim). */
23
+ export declare function errorHeadline(code: string): string;
24
+ /** Google's documentation URLs, per error code (captured from real `authError` payloads). */
25
+ export declare const ERROR_DOC_URLS: Record<string, string>;
@@ -0,0 +1,144 @@
1
+ // Google's OAuth ERROR-PAGE mechanism, reproduced.
2
+ //
3
+ // THE FACT THIS FILE EXISTS FOR: when Google's authorization endpoint rejects a request for a
4
+ // configuration or parameter reason, it does NOT answer with an HTML body and it does NOT redirect
5
+ // to the caller's `redirect_uri`. It 302s to ITS OWN error page:
6
+ //
7
+ // https://accounts.google.com/signin/oauth/error
8
+ // ?authError=<base64url protobuf>&flowName=GeneralOAuthFlow&client_id=<id>
9
+ //
10
+ // and that page renders "Access blocked: …" plus the "Error <status>: <code>" line every integrator
11
+ // ends up searching for. The `authError` payload is a base64url-encoded protobuf with five fields:
12
+ //
13
+ // 1 (string) the OAuth error code e.g. "redirect_uri_mismatch"
14
+ // 2 (string) the human message e.g. "The OAuth client was not found."
15
+ // 3 (string) a documentation URL
16
+ // 4 (varint) the HTTP status the page names e.g. 401
17
+ // 5 (string) the echoed parameter e.g. "redirect_uri=https://evil.test/steal"
18
+ //
19
+ // Reproducing the REDIRECT (rather than serving HTML inline) is what makes the twin's browser
20
+ // behaviour match: the address bar changes to accounts.google.com/signin/oauth/error, which is
21
+ // exactly what a developer sees and screenshots when they file a bug.
22
+ //
23
+ // Grounded by a read-only live capture of the real endpoint on 2026-08-20 (the field numbering was
24
+ // decoded from real `authError` payloads); recorded in spec-sources.json.
25
+ //
26
+ // UNCONFIRMED, and therefore not asserted anywhere: the HTTP STATUS the rendered error page itself
27
+ // is served with. The status in field 4 is the one the page DISPLAYS. The twin serves the page as a
28
+ // normal 200 document and puts the status in the text, which is what the capture shows the page
29
+ // saying; `googleoauth.errors.error_page_http_status` is the filed todo.
30
+ export const ERROR_PAGE_PATH = '/signin/oauth/error';
31
+ /** Google puts this on every OAuth error-page URL; a real capture always carries it. */
32
+ export const FLOW_NAME = 'GeneralOAuthFlow';
33
+ function varint(n) {
34
+ const out = [];
35
+ let v = n;
36
+ while (v > 0x7f) {
37
+ out.push((v & 0x7f) | 0x80);
38
+ v >>>= 7;
39
+ }
40
+ out.push(v);
41
+ return out;
42
+ }
43
+ function stringField(field, value) {
44
+ const bytes = Array.from(Buffer.from(value, 'utf8'));
45
+ return [(field << 3) | 2, ...varint(bytes.length), ...bytes];
46
+ }
47
+ /** Encode an AuthError as the base64url protobuf Google puts in `?authError=`. */
48
+ export function encodeAuthError(e) {
49
+ const out = [
50
+ ...stringField(1, e.code),
51
+ ...stringField(2, e.message),
52
+ ...(e.docUrl ? stringField(3, e.docUrl) : []),
53
+ (4 << 3) | 0,
54
+ ...varint(e.status),
55
+ ...(e.param ? stringField(5, e.param) : []),
56
+ ];
57
+ return Buffer.from(Uint8Array.from(out)).toString('base64url');
58
+ }
59
+ /** Decode a `?authError=` payload back into its fields. Returns null on anything unparseable. */
60
+ export function decodeAuthError(encoded) {
61
+ let buf;
62
+ try {
63
+ buf = Buffer.from(encoded, 'base64url');
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ const out = {};
69
+ let i = 0;
70
+ const readVarint = () => {
71
+ let result = 0;
72
+ let shift = 0;
73
+ while (i < buf.length) {
74
+ const byte = buf[i++];
75
+ result |= (byte & 0x7f) << shift;
76
+ if ((byte & 0x80) === 0)
77
+ return result >>> 0;
78
+ shift += 7;
79
+ if (shift > 28)
80
+ return -1;
81
+ }
82
+ return -1;
83
+ };
84
+ while (i < buf.length) {
85
+ const tag = readVarint();
86
+ if (tag < 0)
87
+ return null;
88
+ const field = tag >>> 3;
89
+ const wire = tag & 7;
90
+ if (wire === 0) {
91
+ const value = readVarint();
92
+ if (value < 0)
93
+ return null;
94
+ if (field === 4)
95
+ out.status = value;
96
+ }
97
+ else if (wire === 2) {
98
+ const len = readVarint();
99
+ if (len < 0 || i + len > buf.length)
100
+ return null;
101
+ const value = buf.subarray(i, i + len).toString('utf8');
102
+ i += len;
103
+ if (field === 1)
104
+ out.code = value;
105
+ else if (field === 2)
106
+ out.message = value;
107
+ else if (field === 3)
108
+ out.docUrl = value;
109
+ else if (field === 5)
110
+ out.param = value;
111
+ }
112
+ else {
113
+ return null; // a wire type Google's payload never uses
114
+ }
115
+ }
116
+ if (!out.code || out.message === undefined || out.status === undefined)
117
+ return null;
118
+ return out;
119
+ }
120
+ /** The full error-page URL an authorization request is bounced to. */
121
+ export function authErrorRedirect(origin, e, clientId) {
122
+ const url = new URL(`${origin}${ERROR_PAGE_PATH}`);
123
+ url.searchParams.set('authError', encodeAuthError(e));
124
+ url.searchParams.set('flowName', FLOW_NAME);
125
+ if (clientId)
126
+ url.searchParams.set('client_id', clientId);
127
+ return url.toString();
128
+ }
129
+ /** The two headline strings Google's error pages use, chosen by error code (captured verbatim). */
130
+ export function errorHeadline(code) {
131
+ // `redirect_uri_mismatch` is an app-configuration failure, and Google words it as one; every
132
+ // other code lands on the generic authorization-error headline.
133
+ return code === 'redirect_uri_mismatch'
134
+ ? "Access blocked: This app's request is invalid"
135
+ : 'Access blocked: Authorization Error';
136
+ }
137
+ /** Google's documentation URLs, per error code (captured from real `authError` payloads). */
138
+ export const ERROR_DOC_URLS = {
139
+ redirect_uri_mismatch: 'https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation',
140
+ invalid_client: 'https://developers.google.com/identity/protocols/oauth2',
141
+ invalid_request: 'https://developers.google.com/identity/protocols/oauth2/web-server',
142
+ invalid_scope: 'https://developers.google.com/identity/protocols/oauth2/scopes',
143
+ unsupported_response_type: 'https://developers.google.com/identity/protocols/oauth2/web-server',
144
+ };
@@ -0,0 +1,48 @@
1
+ import { RateBudget, type RateBudgetDeclaration, type RateBudgetOptions, type RateBudgetReservation, type RateBudgetSnapshot } from '@volter/world-core';
2
+ /** Rolling window, in ms. Spend older than this is pruned. */
3
+ export declare const GOOGLEOAUTH_BUDGET_WINDOW_MS = 60000;
4
+ /**
5
+ * Weighted units allowed inside one window. 60/60s = 30 calls a minute at the default weight —
6
+ * EXACTLY the kernel fallback's burst, because Google publishes no rate this could be measured
7
+ * against. See the header.
8
+ */
9
+ export declare const GOOGLEOAUTH_BUDGET_CEILING = 60;
10
+ /** Seconds. A `Retry-After` above this means the client is throttled hard — fail loudly, don't sleep. */
11
+ export declare const GOOGLEOAUTH_BUDGET_MAX_RETRY_AFTER_S = 300;
12
+ /** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is judged vs. documented. */
13
+ export declare const GOOGLEOAUTH_CALL_WEIGHTS: {
14
+ /** `POST /revoke` — destroys a whole grant on a REAL account. The only destructive call here. */
15
+ readonly revoke: 10;
16
+ /** Everything else: discovery, certs, userinfo, tokeninfo. */
17
+ readonly other: 2;
18
+ };
19
+ /** THE PACK'S DECLARATION — pure data, the only Google-specific thing in the whole budget. */
20
+ export declare const GOOGLEOAUTH_RATE_BUDGET: RateBudgetDeclaration;
21
+ /**
22
+ * Price one call. The key is `"<METHOD> <path>"` with the query string split off. NORMALIZED
23
+ * (upper-cased method, collapsed trailing slash) because the anchored rules are otherwise trivially
24
+ * evaded by input variation: `fetch` upper-cases a known method before sending, so
25
+ * `execute('post', '/revoke')` really does issue the destructive call and must be priced as one.
26
+ */
27
+ export declare function googleOAuthCallWeight(method: string, path: string): number;
28
+ /** Where this vendor's ledger lives. Token-keyed and cwd-independent by default; pass `root` to opt
29
+ * into world-scoped accounting. */
30
+ export declare function googleOAuthBudgetPath(opts?: {
31
+ root?: string;
32
+ token?: string;
33
+ } | string): string;
34
+ /** Construction options. The vendor is fixed; everything else may only TIGHTEN. */
35
+ export type GoogleOAuthBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
36
+ /**
37
+ * This vendor's budget — the shared kernel guard bound to this declaration. A real subclass, not an
38
+ * alias, so `budget instanceof GoogleOAuthBudget` means "a budget that accounts against
39
+ * GOOGLEOAUTH's ledger under GOOGLEOAUTH's ceiling": another vendor's `RateBudget` (with its own,
40
+ * possibly larger, ceiling) is NOT assignable there.
41
+ */
42
+ export declare class GoogleOAuthBudget extends RateBudget {
43
+ constructor(opts?: GoogleOAuthBudgetOptions);
44
+ }
45
+ export { RateBudgetError as GoogleOAuthBudgetError } from '@volter/world-core';
46
+ export type { RateBudgetErrorKind as GoogleOAuthBudgetErrorKind } from '@volter/world-core';
47
+ export type GoogleOAuthBudgetReservation = RateBudgetReservation;
48
+ export type GoogleOAuthBudgetSnapshot = RateBudgetSnapshot;
@@ -0,0 +1,121 @@
1
+ // Google OAuth's CLIENT-SIDE RATE BUDGET — this pack's DECLARATION (the numbers) plus the thin
2
+ // typed bindings `liveGoogleOAuthExecute` uses. The MECHANISM — the durable token-keyed ledger, the
3
+ // rolling window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt
4
+ // ledger — lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → `rateBudget.ts`). Read that
5
+ // module's header for the full rationale AND for the honest list of what the guard does not
6
+ // guarantee (an injected clock or ledger path still defeats it — it guards carelessness, not
7
+ // malice).
8
+ //
9
+ // ── HOW THE CEILING WAS CHOSEN ──────────────────────────────────────────────────────────────
10
+ // GOOGLE PUBLISHES NO SCALAR PER-MINUTE RATE LIMIT for its OAuth 2.0 endpoints. That is the
11
+ // honest finding, and it is the reason this declaration is NOT more permissive than the kernel's
12
+ // undeclared fallback. What Google DOES publish about these endpoints is quota of a different
13
+ // SHAPE entirely — per-user/per-client TOKEN COUNTS, not a request rate:
14
+ // • a Google Account can hold at most 100 refresh tokens per OAuth client; minting the 101st
15
+ // silently invalidates the oldest (developers.google.com/identity/protocols/oauth2, "Refresh
16
+ // token expiration");
17
+ // • a testing-mode app's refresh tokens expire after 7 days;
18
+ // • a client has a cap on unexpired refresh tokens across all users.
19
+ // None of those is a per-minute rate, so none of them can anchor a rolling-window ceiling — and
20
+ // dressing one up as one would be exactly the "never a guess presented as a vendor fact" failure
21
+ // ADDING_A_TWIN.md warns about. The ceiling therefore sits AT the kernel fallback's burst (30 calls
22
+ // a minute at the default weight), which is also why this pack needs no `VENDOR_BURST_ANCHOR`
23
+ // entry in scripts/rate-budget-isolation.test.ts.
24
+ //
25
+ // ── HOW THE WEIGHTS WERE CHOSEN (a judgement call, stated as one) ────────────────────────────
26
+ // The connector only ever READS (there is no OAuth write API — see the connector header), so every
27
+ // call is a read and every read costs the same 2. Exactly ONE call is priced up:
28
+ // • `POST /revoke` costs 10. Revocation is the one destructive thing this surface can do to a
29
+ // real account, and it takes the WHOLE grant with it (every access and refresh token AND the
30
+ // grant record for that client+user). A loop that revokes is a loop that logs a human out of an
31
+ // app, which is a human blast radius rather than a throttle.
32
+ // There is deliberately NO second rule. An earlier version carried `^GET /tokeninfo` at the DEFAULT
33
+ // weight "so the rule exists to be found and re-priced" — but a rule whose weight equals the default
34
+ // changes nothing and CANNOT BE PINNED: delete it and every assertion about it still passes, because
35
+ // the fallback returns the same number (§9 round two). A rule no test can lose is not a rule; if
36
+ // tokeninfo ever needs a different price, the rule arrives WITH the number that makes it testable.
37
+ import { declareRateBudget, rateBudgetPath, rateBudgetWeight, RateBudget, } from '@volter/world-core';
38
+ const VENDOR = 'googleoauth';
39
+ /** Rolling window, in ms. Spend older than this is pruned. */
40
+ export const GOOGLEOAUTH_BUDGET_WINDOW_MS = 60_000;
41
+ /**
42
+ * Weighted units allowed inside one window. 60/60s = 30 calls a minute at the default weight —
43
+ * EXACTLY the kernel fallback's burst, because Google publishes no rate this could be measured
44
+ * against. See the header.
45
+ */
46
+ export const GOOGLEOAUTH_BUDGET_CEILING = 60;
47
+ /** Seconds. A `Retry-After` above this means the client is throttled hard — fail loudly, don't sleep. */
48
+ export const GOOGLEOAUTH_BUDGET_MAX_RETRY_AFTER_S = 300;
49
+ /** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is judged vs. documented. */
50
+ export const GOOGLEOAUTH_CALL_WEIGHTS = {
51
+ /** `POST /revoke` — destroys a whole grant on a REAL account. The only destructive call here. */
52
+ revoke: 10,
53
+ /** Everything else: discovery, certs, userinfo, tokeninfo. */
54
+ other: 2,
55
+ };
56
+ /** THE PACK'S DECLARATION — pure data, the only Google-specific thing in the whole budget. */
57
+ export const GOOGLEOAUTH_RATE_BUDGET = {
58
+ windowMs: GOOGLEOAUTH_BUDGET_WINDOW_MS,
59
+ ceiling: GOOGLEOAUTH_BUDGET_CEILING,
60
+ defaultWeight: GOOGLEOAUTH_CALL_WEIGHTS.other,
61
+ maxRetryAfterSeconds: GOOGLEOAUTH_BUDGET_MAX_RETRY_AFTER_S,
62
+ rules: [{ match: '^POST /revoke$', weight: GOOGLEOAUTH_CALL_WEIGHTS.revoke }],
63
+ reason: 'Google publishes NO scalar per-minute rate limit for its OAuth 2.0 / OIDC endpoints '
64
+ + '(accounts.google.com, oauth2.googleapis.com, www.googleapis.com/oauth2/*). What it does '
65
+ + 'publish for this surface is quota of a different SHAPE — per-user/per-client TOKEN COUNTS, '
66
+ + 'not a request rate: at most 100 refresh tokens per Google Account per OAuth client (the '
67
+ + '101st silently invalidates the oldest), a 7-day refresh-token expiry for apps still in '
68
+ + 'testing status, and a per-client cap on unexpired refresh tokens '
69
+ + '(developers.google.com/identity/protocols/oauth2, "Refresh token expiration"). A token-count '
70
+ + 'cap cannot anchor a rolling-window rate, so this declaration does NOT claim it does and does '
71
+ + "NOT out-burst the kernel's undeclared fallback: 60 weighted units / 60s at defaultWeight 2 is "
72
+ + '30 calls a minute, exactly the fallback. POST /revoke costs 10 (it destroys the WHOLE grant '
73
+ + 'on a real account — every access and refresh token for that client+user — which is a human '
74
+ + 'blast radius, not a throttle). It is the ONLY rule: a rule at the default weight changes no '
75
+ + 'price and cannot be pinned by any test, so there is none. That price is a judgement call, not '
76
+ + 'a published cost. The window bounds the 60s AVERAGE and does not pace; the 429 / Retry-After '
77
+ + 'cooldown is the backstop for a sub-second burst.',
78
+ };
79
+ // Declared at module load, so merely importing this module (which `googleoauth-connector.ts` does)
80
+ // is enough to arm the real ceiling.
81
+ declareRateBudget(VENDOR, GOOGLEOAUTH_RATE_BUDGET);
82
+ /**
83
+ * Price one call. The key is `"<METHOD> <path>"` with the query string split off. NORMALIZED
84
+ * (upper-cased method, collapsed trailing slash) because the anchored rules are otherwise trivially
85
+ * evaded by input variation: `fetch` upper-cases a known method before sending, so
86
+ * `execute('post', '/revoke')` really does issue the destructive call and must be priced as one.
87
+ */
88
+ export function googleOAuthCallWeight(method, path) {
89
+ const { bare, query } = splitQuery(path);
90
+ return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
91
+ }
92
+ function splitQuery(path) {
93
+ const at = path.indexOf('?');
94
+ const query = {};
95
+ if (at !== -1)
96
+ for (const [k, v] of new URLSearchParams(path.slice(at + 1)))
97
+ query[k] = v;
98
+ const raw = at === -1 ? path : path.slice(0, at);
99
+ const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
100
+ return { bare, query };
101
+ }
102
+ /** Where this vendor's ledger lives. Token-keyed and cwd-independent by default; pass `root` to opt
103
+ * into world-scoped accounting. */
104
+ export function googleOAuthBudgetPath(opts = {}) {
105
+ const o = typeof opts === 'string' ? { root: opts } : opts;
106
+ // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
107
+ // excess-property check only catches object literals) must not redirect this pack's ledger.
108
+ return rateBudgetPath({ ...o, vendor: VENDOR });
109
+ }
110
+ /**
111
+ * This vendor's budget — the shared kernel guard bound to this declaration. A real subclass, not an
112
+ * alias, so `budget instanceof GoogleOAuthBudget` means "a budget that accounts against
113
+ * GOOGLEOAUTH's ledger under GOOGLEOAUTH's ceiling": another vendor's `RateBudget` (with its own,
114
+ * possibly larger, ceiling) is NOT assignable there.
115
+ */
116
+ export class GoogleOAuthBudget extends RateBudget {
117
+ constructor(opts = {}) {
118
+ super({ ...opts, vendor: VENDOR });
119
+ }
120
+ }
121
+ export { RateBudgetError as GoogleOAuthBudgetError } from '@volter/world-core';
@@ -0,0 +1,3 @@
1
+ import { type CapabilityReport, type CapabilitySpec } from '@volter/world-tooling';
2
+ export declare const GOOGLEOAUTH_CAPABILITIES: CapabilitySpec[];
3
+ export declare function googleoauthCapabilities(): Promise<CapabilityReport>;