@bevel-software/platform-mcp-core 0.24.0 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/google-service-account/google-auth-http.protocol.d.ts +24 -0
  2. package/dist/google-service-account/google-auth-http.protocol.d.ts.map +1 -0
  3. package/dist/google-service-account/google-auth-http.protocol.js +47 -0
  4. package/dist/google-service-account/google-auth-http.protocol.js.map +1 -0
  5. package/dist/google-service-account/google-service-account.auth.d.ts +45 -0
  6. package/dist/google-service-account/google-service-account.auth.d.ts.map +1 -0
  7. package/dist/google-service-account/google-service-account.auth.js +153 -0
  8. package/dist/google-service-account/google-service-account.auth.js.map +1 -0
  9. package/dist/google-service-account/google-service-account.token-source.d.ts +28 -0
  10. package/dist/google-service-account/google-service-account.token-source.d.ts.map +1 -0
  11. package/dist/google-service-account/google-service-account.token-source.js +206 -0
  12. package/dist/google-service-account/google-service-account.token-source.js.map +1 -0
  13. package/dist/google-service-account/index.d.ts +5 -0
  14. package/dist/google-service-account/index.d.ts.map +1 -0
  15. package/dist/google-service-account/index.js +7 -0
  16. package/dist/google-service-account/index.js.map +1 -0
  17. package/dist/google-service-account/service-account-token.contract.d.ts +33 -0
  18. package/dist/google-service-account/service-account-token.contract.d.ts.map +1 -0
  19. package/dist/google-service-account/service-account-token.contract.js +14 -0
  20. package/dist/google-service-account/service-account-token.contract.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +5 -0
  24. package/dist/index.js.map +1 -1
  25. package/package.json +3 -2
  26. package/src/google-service-account/google-auth-http.protocol.ts +62 -0
  27. package/src/google-service-account/google-service-account.auth.ts +161 -0
  28. package/src/google-service-account/google-service-account.token-source.ts +239 -0
  29. package/src/google-service-account/index.ts +15 -0
  30. package/src/google-service-account/service-account-token.contract.ts +38 -0
  31. package/src/index.ts +18 -0
@@ -0,0 +1,24 @@
1
+ import { type CallTemplate, type IUtcpClient } from '@utcp/sdk';
2
+ import { HttpCommunicationProtocol } from '@utcp/http';
3
+ import type { IServiceAccountTokenSource } from './service-account-token.contract.js';
4
+ /**
5
+ * The stock UTCP `http` protocol, taught one more auth type. A call whose
6
+ * template names `google_service_account` is handed to the stock protocol with
7
+ * that auth swapped for the bearer token it resolves to (as `oauth2_user`,
8
+ * which the stock protocol sends as `Authorization: Bearer <token>` and strips
9
+ * on a cross-origin redirect). Every other call passes through untouched.
10
+ */
11
+ export declare class GoogleAuthHttpProtocol extends HttpCommunicationProtocol {
12
+ private readonly tokens;
13
+ constructor(tokens: IServiceAccountTokenSource);
14
+ callTool(caller: IUtcpClient, toolName: string, toolArgs: Record<string, unknown>, toolCallTemplate: CallTemplate): Promise<unknown>;
15
+ callToolStreaming(caller: IUtcpClient, toolName: string, toolArgs: Record<string, unknown>, toolCallTemplate: CallTemplate): AsyncGenerator<unknown, void, unknown>;
16
+ private withBearerToken;
17
+ }
18
+ /**
19
+ * Put the service-account-aware protocol in place of the stock `http` one,
20
+ * minting tokens from `tokens`. Exported so a suite can answer for Google;
21
+ * a process never needs to call it, since loading this module already has.
22
+ */
23
+ export declare function installGoogleServiceAccountAuth(tokens?: IServiceAccountTokenSource): void;
24
+ //# sourceMappingURL=google-auth-http.protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-auth-http.protocol.d.ts","sourceRoot":"","sources":["../../src/google-service-account/google-auth-http.protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,yBAAyB,EAAyB,MAAM,YAAY,CAAC;AAG9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AAEtF;;;;;;GAMG;AACH,qBAAa,sBAAuB,SAAQ,yBAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,0BAA0B;IAIhD,QAAQ,CACrB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,gBAAgB,EAAE,YAAY,GAC7B,OAAO,CAAC,OAAO,CAAC;IAIH,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,gBAAgB,EAAE,YAAY,GAC7B,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC;YAI3B,eAAe;CAM9B;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,GAAE,0BAAkE,GACzE,IAAI,CAEN"}
@@ -0,0 +1,47 @@
1
+ import { CommunicationProtocol } from '@utcp/sdk';
2
+ import { HttpCommunicationProtocol } from '@utcp/http';
3
+ import { isGoogleServiceAccountAuth } from './google-service-account.auth.js';
4
+ import { GoogleServiceAccountTokenSource } from './google-service-account.token-source.js';
5
+ /**
6
+ * The stock UTCP `http` protocol, taught one more auth type. A call whose
7
+ * template names `google_service_account` is handed to the stock protocol with
8
+ * that auth swapped for the bearer token it resolves to (as `oauth2_user`,
9
+ * which the stock protocol sends as `Authorization: Bearer <token>` and strips
10
+ * on a cross-origin redirect). Every other call passes through untouched.
11
+ */
12
+ export class GoogleAuthHttpProtocol extends HttpCommunicationProtocol {
13
+ tokens;
14
+ constructor(tokens) {
15
+ super();
16
+ this.tokens = tokens;
17
+ }
18
+ async callTool(caller, toolName, toolArgs, toolCallTemplate) {
19
+ return super.callTool(caller, toolName, toolArgs, await this.withBearerToken(toolCallTemplate));
20
+ }
21
+ async *callToolStreaming(caller, toolName, toolArgs, toolCallTemplate) {
22
+ yield* super.callToolStreaming(caller, toolName, toolArgs, await this.withBearerToken(toolCallTemplate));
23
+ }
24
+ async withBearerToken(template) {
25
+ const auth = template.auth;
26
+ if (!isGoogleServiceAccountAuth(auth))
27
+ return template;
28
+ const accessToken = await this.tokens.accessToken(auth);
29
+ return { ...template, auth: { auth_type: 'oauth2_user', access_token: accessToken } };
30
+ }
31
+ }
32
+ /**
33
+ * Put the service-account-aware protocol in place of the stock `http` one,
34
+ * minting tokens from `tokens`. Exported so a suite can answer for Google;
35
+ * a process never needs to call it, since loading this module already has.
36
+ */
37
+ export function installGoogleServiceAccountAuth(tokens = new GoogleServiceAccountTokenSource()) {
38
+ CommunicationProtocol.communicationProtocols['http'] = new GoogleAuthHttpProtocol(tokens);
39
+ }
40
+ // Installed on module load, the way `@utcp/http` installs the protocol this
41
+ // one replaces: once per process, before any client exists (a client copies
42
+ // the registry when it is built). The import of `@utcp/http` above has already
43
+ // run its own registration by the time this line does, whatever order the
44
+ // importing module lists the two in. One token cache then serves the process;
45
+ // its entries are keyed by the key itself, so knowledge bases never share one.
46
+ installGoogleServiceAccountAuth();
47
+ //# sourceMappingURL=google-auth-http.protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-auth-http.protocol.js","sourceRoot":"","sources":["../../src/google-service-account/google-auth-http.protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAuC,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,yBAAyB,EAAyB,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAC9E,OAAO,EAAE,+BAA+B,EAAE,MAAM,0CAA0C,CAAC;AAG3F;;;;;;GAMG;AACH,MAAM,OAAO,sBAAuB,SAAQ,yBAAyB;IACtC;IAA7B,YAA6B,MAAkC;QAC7D,KAAK,EAAE,CAAC;QADmB,WAAM,GAAN,MAAM,CAA4B;IAE/D,CAAC;IAEQ,KAAK,CAAC,QAAQ,CACrB,MAAmB,EACnB,QAAgB,EAChB,QAAiC,EACjC,gBAA8B;QAE9B,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAClG,CAAC;IAEQ,KAAK,CAAC,CAAC,iBAAiB,CAC/B,MAAmB,EACnB,QAAgB,EAChB,QAAiC,EACjC,gBAA8B;QAE9B,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC3G,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,QAAsB;QAClD,MAAM,IAAI,GAAI,QAA6B,CAAC,IAAI,CAAC;QACjD,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC;YAAE,OAAO,QAAQ,CAAC;QACvD,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACxD,OAAO,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,EAAkB,CAAC;IACxG,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,SAAqC,IAAI,+BAA+B,EAAE;IAE1E,qBAAqB,CAAC,sBAAsB,CAAC,MAAM,CAAC,GAAG,IAAI,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC5F,CAAC;AAED,4EAA4E;AAC5E,4EAA4E;AAC5E,+EAA+E;AAC/E,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,+BAA+B,EAAE,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { type GoogleServiceAccountAuth } from './service-account-token.contract.js';
2
+ /** Whether a call template's `auth` asks for a Google service-account token. */
3
+ export declare function isGoogleServiceAccountAuth(auth: unknown): auth is GoogleServiceAccountAuth;
4
+ /**
5
+ * Where in `doc` a `google_service_account` auth block sits that nothing will
6
+ * act on, as a phrase for a refusal, or null when every block is the `auth` of
7
+ * one of `toolCallTemplates` and that template is an `http` one.
8
+ *
9
+ * `toolCallTemplates` are the templates tools are really called through, which
10
+ * only the caller knows: the document's shape is its business. A block
11
+ * anywhere else is read by nothing, whatever the object around it looks like:
12
+ * a template the document never registers, an `auth_tools`, a block at the
13
+ * root of a file that discovers its tools from a url.
14
+ *
15
+ * UTCP validates an auth type on any call template that takes an `auth`, but
16
+ * only the `http` protocol mints the token. Every other protocol sends an auth
17
+ * type it does not know as no credentials at all, so such a tool would save
18
+ * cleanly and then call Google unauthenticated.
19
+ *
20
+ * Every block in the document is found, at any depth (see
21
+ * `googleServiceAccountBlocks`), and the first one in document order that
22
+ * nothing will act on is the one named.
23
+ */
24
+ export declare function findUnservedGoogleServiceAccountAuth(doc: unknown, toolCallTemplates: readonly unknown[]): string | null;
25
+ /**
26
+ * Whether any `google_service_account` block in `doc` has its key WRITTEN IN
27
+ * THE DOCUMENT: a `credentials` that is anything but one variable reference.
28
+ *
29
+ * The document is a `.tool`, which is knowledge-base content: it is committed
30
+ * to the repository, and read by everyone and every agent that can read the
31
+ * knowledge base. A key written into it is a key all of them hold, for as
32
+ * long as the history keeps it. So the place for the key is the vault, and
33
+ * `credentials` only ever names the variable.
34
+ *
35
+ * Asked of the document as its author wrote it, wherever the block sits: a
36
+ * key in a block nothing will act on is just as readable. It cannot be asked
37
+ * where the auth type itself is validated, because that also runs on the
38
+ * template AFTER its variables were substituted, where `credentials` is the
39
+ * key and has to be.
40
+ *
41
+ * Says only that one was found. It never returns, quotes or measures the
42
+ * value: what it would be describing is the secret.
43
+ */
44
+ export declare function holdsLiteralGoogleServiceAccountKey(doc: unknown): boolean;
45
+ //# sourceMappingURL=google-service-account.auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-service-account.auth.d.ts","sourceRoot":"","sources":["../../src/google-service-account/google-service-account.auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AA0BtH,gFAAgF;AAChF,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,wBAAwB,CAM1F;AAYD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oCAAoC,CAAC,GAAG,EAAE,OAAO,EAAE,iBAAiB,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI,CASvH;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAOzE"}
@@ -0,0 +1,153 @@
1
+ import { AuthSerializer, Serializer } from '@utcp/sdk';
2
+ import { z } from 'zod';
3
+ import { GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE } from './service-account-token.contract.js';
4
+ const GoogleServiceAccountAuthSchema = z.object({
5
+ auth_type: z.literal(GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE),
6
+ credentials: z
7
+ .string()
8
+ .min(1)
9
+ .describe('The service-account key JSON. Recommended to use a vault variable like "${GOOGLE_SA_KEY}".'),
10
+ // Trimmed before the length check, so a blank scope is refused here rather
11
+ // than reaching Google as an empty one.
12
+ scopes: z
13
+ .union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)])
14
+ .describe('OAuth scopes for the token, e.g. "https://www.googleapis.com/auth/adwords".'),
15
+ subject: z.string().trim().min(1).optional().describe('User to impersonate under domain-wide delegation.'),
16
+ });
17
+ class GoogleServiceAccountAuthSerializer extends Serializer {
18
+ toDict(obj) {
19
+ return { ...obj };
20
+ }
21
+ validateDict(obj) {
22
+ return GoogleServiceAccountAuthSchema.parse(obj);
23
+ }
24
+ }
25
+ /** Whether a call template's `auth` asks for a Google service-account token. */
26
+ export function isGoogleServiceAccountAuth(auth) {
27
+ return (typeof auth === 'object' &&
28
+ auth !== null &&
29
+ auth.auth_type === GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE);
30
+ }
31
+ /** The one call template type whose protocol turns the key into a token. */
32
+ const SERVED_CALL_TEMPLATE_TYPE = 'http';
33
+ /**
34
+ * The other call template types an author is likely to have reached for, which
35
+ * the answer below may name. A type outside this list is the file's own text,
36
+ * and is described rather than quoted back.
37
+ */
38
+ const NAMEABLE_CALL_TEMPLATE_TYPES = ['sse', 'streamable_http', 'mcp', 'cli'];
39
+ /**
40
+ * Where in `doc` a `google_service_account` auth block sits that nothing will
41
+ * act on, as a phrase for a refusal, or null when every block is the `auth` of
42
+ * one of `toolCallTemplates` and that template is an `http` one.
43
+ *
44
+ * `toolCallTemplates` are the templates tools are really called through, which
45
+ * only the caller knows: the document's shape is its business. A block
46
+ * anywhere else is read by nothing, whatever the object around it looks like:
47
+ * a template the document never registers, an `auth_tools`, a block at the
48
+ * root of a file that discovers its tools from a url.
49
+ *
50
+ * UTCP validates an auth type on any call template that takes an `auth`, but
51
+ * only the `http` protocol mints the token. Every other protocol sends an auth
52
+ * type it does not know as no credentials at all, so such a tool would save
53
+ * cleanly and then call Google unauthenticated.
54
+ *
55
+ * Every block in the document is found, at any depth (see
56
+ * `googleServiceAccountBlocks`), and the first one in document order that
57
+ * nothing will act on is the one named.
58
+ */
59
+ export function findUnservedGoogleServiceAccountAuth(doc, toolCallTemplates) {
60
+ const served = new Set(toolCallTemplates);
61
+ for (const { parent, key } of googleServiceAccountBlocks(doc)) {
62
+ // Judged by where it sits, so once per place it appears: an aliased
63
+ // block can be served in one place and ignored in another.
64
+ const where = unservedPlacement(parent, key, served);
65
+ if (where)
66
+ return where;
67
+ }
68
+ return null;
69
+ }
70
+ /** `${NAME}` or `$NAME`, as UTCP spells a variable, and nothing else around it. */
71
+ const ONE_VARIABLE_REFERENCE = /^\s*(?:\$\{[a-zA-Z0-9_]+\}|\$[a-zA-Z0-9_]+)\s*$/;
72
+ /**
73
+ * Whether any `google_service_account` block in `doc` has its key WRITTEN IN
74
+ * THE DOCUMENT: a `credentials` that is anything but one variable reference.
75
+ *
76
+ * The document is a `.tool`, which is knowledge-base content: it is committed
77
+ * to the repository, and read by everyone and every agent that can read the
78
+ * knowledge base. A key written into it is a key all of them hold, for as
79
+ * long as the history keeps it. So the place for the key is the vault, and
80
+ * `credentials` only ever names the variable.
81
+ *
82
+ * Asked of the document as its author wrote it, wherever the block sits: a
83
+ * key in a block nothing will act on is just as readable. It cannot be asked
84
+ * where the auth type itself is validated, because that also runs on the
85
+ * template AFTER its variables were substituted, where `credentials` is the
86
+ * key and has to be.
87
+ *
88
+ * Says only that one was found. It never returns, quotes or measures the
89
+ * value: what it would be describing is the secret.
90
+ */
91
+ export function holdsLiteralGoogleServiceAccountKey(doc) {
92
+ for (const { block } of googleServiceAccountBlocks(doc)) {
93
+ const { credentials } = block;
94
+ // Not a string is not a key either; the auth type's own schema refuses it.
95
+ if (typeof credentials === 'string' && !ONE_VARIABLE_REFERENCE.test(credentials))
96
+ return true;
97
+ }
98
+ return false;
99
+ }
100
+ /**
101
+ * Every `google_service_account` auth block in `doc`, at any depth, with the
102
+ * object it sits in and the key it sits under, in document order.
103
+ *
104
+ * The walk keeps its own stack, so a deeply nested document cannot overflow
105
+ * the call stack, and it does not re-enter an object it has seen: a YAML
106
+ * anchor aliased inside itself parses to a cyclic object. A block is yielded
107
+ * once per place it appears, since an aliased one sits in several.
108
+ */
109
+ function* googleServiceAccountBlocks(doc) {
110
+ const seen = new WeakSet();
111
+ const pending = [{ node: doc }];
112
+ for (let next = pending.pop(); next; next = pending.pop()) {
113
+ const { node, parent, key } = next;
114
+ if (!node || typeof node !== 'object')
115
+ continue;
116
+ if (isGoogleServiceAccountAuth(node)) {
117
+ yield { block: node, parent, key };
118
+ continue;
119
+ }
120
+ if (seen.has(node))
121
+ continue;
122
+ seen.add(node);
123
+ // Pushed in reverse, so blocks come out in document order.
124
+ if (Array.isArray(node)) {
125
+ for (let i = node.length - 1; i >= 0; i--)
126
+ pending.push({ node: node[i] });
127
+ }
128
+ else {
129
+ const obj = node;
130
+ const keys = Object.keys(obj);
131
+ for (let i = keys.length - 1; i >= 0; i--)
132
+ pending.push({ node: obj[keys[i]], parent: obj, key: keys[i] });
133
+ }
134
+ }
135
+ }
136
+ /** Why a block under `parent[key]` is acted on by nothing, or null when it is. */
137
+ function unservedPlacement(parent, key, served) {
138
+ if (!parent || key !== 'auth' || typeof parent.call_template_type !== 'string') {
139
+ return "somewhere that is not a call template's `auth`";
140
+ }
141
+ const type = parent.call_template_type.toLowerCase().trim();
142
+ if (type !== SERVED_CALL_TEMPLATE_TYPE) {
143
+ return NAMEABLE_CALL_TEMPLATE_TYPES.includes(type) ? `a \`${type}\` call template` : 'a call template that is not an `http` one';
144
+ }
145
+ return served.has(parent) ? null : 'an `http` call template that no tool is called through';
146
+ }
147
+ // Register the auth type on module load, so a `.tool` naming it validates
148
+ // wherever UTCP parses a call template: the inline manual route, the preview,
149
+ // and the client re-validating a template after substituting its variables.
150
+ // UTCP's registry is process-wide and the type carries no state, so one
151
+ // registration serves every knowledge base. Idempotent (safe under hot-reload).
152
+ AuthSerializer.registerAuth(GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE, new GoogleServiceAccountAuthSerializer(), true);
153
+ //# sourceMappingURL=google-service-account.auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-service-account.auth.js","sourceRoot":"","sources":["../../src/google-service-account/google-service-account.auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type GoogleServiceAccountAuth, type IServiceAccountTokenSource } from './service-account-token.contract.js';
2
+ /**
3
+ * Google's token endpoint. Fixed rather than read from the key's own
4
+ * `token_uri`, so a key cannot point the server at another host with a signed
5
+ * assertion in hand.
6
+ */
7
+ export declare const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
8
+ /**
9
+ * Mints and caches access tokens for Google service accounts: signs an RS256
10
+ * assertion with the key's private key, exchanges it at Google's token
11
+ * endpoint, and keeps the token until shortly before it expires. Concurrent
12
+ * calls for the same token share one exchange, and a failed exchange is
13
+ * answered from memory for a few seconds (or for Google's `Retry-After`), so
14
+ * an outage or a rate limit is not met with a fresh exchange on every call.
15
+ * Injected with `fetch` and a clock so suites answer for Google.
16
+ */
17
+ export declare class GoogleServiceAccountTokenSource implements IServiceAccountTokenSource {
18
+ private readonly fetchImpl;
19
+ private readonly now;
20
+ private readonly timeoutMs;
21
+ private readonly tokens;
22
+ private readonly failures;
23
+ private readonly inFlight;
24
+ constructor(fetchImpl?: typeof fetch, now?: () => number, timeoutMs?: number);
25
+ accessToken(auth: GoogleServiceAccountAuth): Promise<string>;
26
+ private exchange;
27
+ }
28
+ //# sourceMappingURL=google-service-account.token-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-service-account.token-source.d.ts","sourceRoot":"","sources":["../../src/google-service-account/google-service-account.token-source.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAChC,MAAM,qCAAqC,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,wCAAwC,CAAC;AA8BtE;;;;;;;;GAQG;AACH,qBAAa,+BAAgC,YAAW,0BAA0B;IAM9E,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAP5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;gBAG5C,SAAS,GAAE,OAAO,KAAa,EAC/B,GAAG,GAAE,MAAM,MAAiB,EAC5B,SAAS,GAAE,MAA2B;IAGnD,WAAW,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC;YA4BpD,QAAQ;CA+CvB"}
@@ -0,0 +1,206 @@
1
+ import { createHash, createSign } from 'node:crypto';
2
+ import { ServiceAccountAuthError, } from './service-account-token.contract.js';
3
+ /**
4
+ * Google's token endpoint. Fixed rather than read from the key's own
5
+ * `token_uri`, so a key cannot point the server at another host with a signed
6
+ * assertion in hand.
7
+ */
8
+ export const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
9
+ const JWT_BEARER_GRANT = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
10
+ /** The longest assertion lifetime Google accepts. */
11
+ const ASSERTION_LIFETIME_S = 3600;
12
+ /** A token this close to expiry is replaced, so a call never leaves with one that dies on the way. */
13
+ const REFRESH_SKEW_MS = 60_000;
14
+ const REQUEST_TIMEOUT_MS = 10_000;
15
+ /** Tokens (and failures) kept at once. Each distinct key, scope set and subject is one entry. */
16
+ const MAX_CACHED_TOKENS = 256;
17
+ /** How long a failed exchange is answered from memory when Google names no `Retry-After`. */
18
+ const FAILURE_BACKOFF_MS = 5_000;
19
+ /** The longest a `Retry-After` from Google holds calls back, so a far-off value cannot park a tool for good. */
20
+ const MAX_FAILURE_BACKOFF_MS = 60_000;
21
+ /**
22
+ * Mints and caches access tokens for Google service accounts: signs an RS256
23
+ * assertion with the key's private key, exchanges it at Google's token
24
+ * endpoint, and keeps the token until shortly before it expires. Concurrent
25
+ * calls for the same token share one exchange, and a failed exchange is
26
+ * answered from memory for a few seconds (or for Google's `Retry-After`), so
27
+ * an outage or a rate limit is not met with a fresh exchange on every call.
28
+ * Injected with `fetch` and a clock so suites answer for Google.
29
+ */
30
+ export class GoogleServiceAccountTokenSource {
31
+ fetchImpl;
32
+ now;
33
+ timeoutMs;
34
+ tokens = new Map();
35
+ failures = new Map();
36
+ inFlight = new Map();
37
+ constructor(fetchImpl = fetch, now = Date.now, timeoutMs = REQUEST_TIMEOUT_MS) {
38
+ this.fetchImpl = fetchImpl;
39
+ this.now = now;
40
+ this.timeoutMs = timeoutMs;
41
+ }
42
+ async accessToken(auth) {
43
+ const key = parseServiceAccountKey(auth.credentials);
44
+ const scope = scopeString(auth.scopes);
45
+ const cacheKey = tokenCacheKey(key, scope, auth.subject);
46
+ const cached = this.tokens.get(cacheKey);
47
+ if (cached && cached.expiresAt - REFRESH_SKEW_MS > this.now())
48
+ return cached.accessToken;
49
+ const failed = this.failures.get(cacheKey);
50
+ if (failed && failed.retryAt > this.now())
51
+ throw failed.error;
52
+ const pending = this.inFlight.get(cacheKey);
53
+ if (pending)
54
+ return pending;
55
+ const exchange = this.exchange(key, scope, auth.subject, cacheKey)
56
+ .catch((err) => {
57
+ // Remembered per key, scope set and subject: a corrected key hashes to
58
+ // another entry, so fixing it is never held back by this.
59
+ if (err instanceof ServiceAccountAuthError) {
60
+ remember(this.failures, cacheKey, { error: err, retryAt: this.now() + err.retryAfterMs });
61
+ }
62
+ throw err;
63
+ })
64
+ .finally(() => this.inFlight.delete(cacheKey));
65
+ this.inFlight.set(cacheKey, exchange);
66
+ return exchange;
67
+ }
68
+ async exchange(key, scope, subject, cacheKey) {
69
+ const assertion = signAssertion(key, scope, subject, this.now());
70
+ let res;
71
+ try {
72
+ res = await this.fetchImpl(GOOGLE_TOKEN_URL, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
75
+ body: new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion }).toString(),
76
+ // Don't follow redirects: the assertion goes to Google's endpoint and nowhere else.
77
+ redirect: 'error',
78
+ signal: AbortSignal.timeout(this.timeoutMs),
79
+ });
80
+ }
81
+ catch (err) {
82
+ const reason = err instanceof Error && err.name === 'TimeoutError'
83
+ ? `timed out after ${this.timeoutMs}ms`
84
+ : err instanceof Error
85
+ ? err.message
86
+ : String(err);
87
+ throw new ServiceAccountAuthError(`Google's token endpoint could not be reached: ${reason}`);
88
+ }
89
+ // Whatever came back is read as an object of fields or as none. A body
90
+ // that is not JSON, and one that is JSON but no object (`null`, a number,
91
+ // a list), both carry no field to read: reading one off `null` would
92
+ // throw a TypeError instead of the refusal below, past the code that
93
+ // remembers a failure and says who was refused.
94
+ const parsed = await res.json().catch(() => null);
95
+ const body = (parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {});
96
+ if (!res.ok) {
97
+ // Google says why in `error` and `error_description` (a revoked key, a
98
+ // scope the account may not have, a subject without delegation); neither
99
+ // carries the key, so both are passed on.
100
+ const detail = [body.error, body.error_description].filter((v) => typeof v === 'string').join(': ');
101
+ throw new ServiceAccountAuthError(`Google refused the service account ${key.clientEmail} (HTTP ${res.status})${detail ? `: ${detail}` : ''}`, retryAfterMs(res.headers.get('retry-after'), this.now()));
102
+ }
103
+ const accessToken = typeof body.access_token === 'string' ? body.access_token : '';
104
+ if (!accessToken)
105
+ throw new ServiceAccountAuthError("Google's token response had no access_token");
106
+ const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : ASSERTION_LIFETIME_S;
107
+ this.failures.delete(cacheKey);
108
+ remember(this.tokens, cacheKey, { accessToken, expiresAt: this.now() + expiresIn * 1000 });
109
+ return accessToken;
110
+ }
111
+ }
112
+ /**
113
+ * Keep `value` under `cacheKey`, dropping the oldest entry first when the map
114
+ * is full: a Map iterates in insertion order, and re-inserting moves an entry
115
+ * to the back.
116
+ */
117
+ function remember(map, cacheKey, value) {
118
+ map.delete(cacheKey);
119
+ if (map.size >= MAX_CACHED_TOKENS) {
120
+ const oldest = map.keys().next().value;
121
+ if (oldest !== undefined)
122
+ map.delete(oldest);
123
+ }
124
+ map.set(cacheKey, value);
125
+ }
126
+ /**
127
+ * How long to hold calls back after a refusal: Google's `Retry-After` (seconds
128
+ * or an HTTP date) when it sent one, capped, else the default backoff.
129
+ */
130
+ function retryAfterMs(header, now) {
131
+ if (!header)
132
+ return FAILURE_BACKOFF_MS;
133
+ const seconds = Number(header);
134
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(header) - now;
135
+ if (!Number.isFinite(ms) || ms <= 0)
136
+ return FAILURE_BACKOFF_MS;
137
+ return Math.min(ms, MAX_FAILURE_BACKOFF_MS);
138
+ }
139
+ /**
140
+ * The key as an admin is likely to have stored it: the JSON file Google
141
+ * issued, or that JSON in base64. Only the fields the assertion needs are
142
+ * kept, and no error quotes what was stored.
143
+ */
144
+ function parseServiceAccountKey(raw) {
145
+ const value = raw.trim();
146
+ let parsed;
147
+ try {
148
+ parsed = JSON.parse(value.startsWith('{') ? value : Buffer.from(value, 'base64').toString('utf8'));
149
+ }
150
+ catch {
151
+ throw new ServiceAccountAuthError('The service-account credentials are not a key JSON. Store the JSON file Google issued for the service account.');
152
+ }
153
+ const obj = (parsed ?? {});
154
+ const clientEmail = typeof obj.client_email === 'string' ? obj.client_email : '';
155
+ const privateKey = typeof obj.private_key === 'string' ? obj.private_key : '';
156
+ if (!clientEmail || !privateKey) {
157
+ throw new ServiceAccountAuthError('The service-account key JSON has no client_email or private_key. Store the JSON file Google issued for the service account.');
158
+ }
159
+ return {
160
+ clientEmail,
161
+ privateKey,
162
+ privateKeyId: typeof obj.private_key_id === 'string' ? obj.private_key_id : undefined,
163
+ };
164
+ }
165
+ /**
166
+ * The scopes as Google wants them: space-separated, each trimmed. UTCP keeps
167
+ * the template as written once it validates, so stray whitespace is cleaned
168
+ * here, whether it sits in a list entry or in a space-separated string.
169
+ */
170
+ function scopeString(scopes) {
171
+ const list = Array.isArray(scopes) ? scopes : [scopes];
172
+ return list
173
+ .flatMap((s) => s.split(/\s+/))
174
+ .filter(Boolean)
175
+ .join(' ');
176
+ }
177
+ /** One entry per key, scope set and subject. The private key is hashed so the cache holds no copy of it. */
178
+ function tokenCacheKey(key, scope, subject) {
179
+ const keyHash = createHash('sha256').update(key.privateKey).digest('hex');
180
+ return JSON.stringify([key.clientEmail, key.privateKeyId ?? '', keyHash, scope.split(' ').sort().join(' '), subject ?? '']);
181
+ }
182
+ /** The RS256 assertion Google exchanges for an access token. */
183
+ function signAssertion(key, scope, subject, now) {
184
+ const seconds = Math.floor(now / 1000);
185
+ const part = (value) => Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
186
+ const header = { alg: 'RS256', typ: 'JWT', ...(key.privateKeyId ? { kid: key.privateKeyId } : {}) };
187
+ const claims = {
188
+ iss: key.clientEmail,
189
+ scope,
190
+ aud: GOOGLE_TOKEN_URL,
191
+ iat: seconds,
192
+ exp: seconds + ASSERTION_LIFETIME_S,
193
+ ...(subject ? { sub: subject } : {}),
194
+ };
195
+ const body = `${part(header)}.${part(claims)}`;
196
+ let signature;
197
+ try {
198
+ signature = createSign('RSA-SHA256').update(body).end().sign(key.privateKey, 'base64url');
199
+ }
200
+ catch {
201
+ // What the key was is never quoted.
202
+ throw new ServiceAccountAuthError(`The private key of service account ${key.clientEmail} could not be read. It must be the PEM in the key JSON Google issued.`);
203
+ }
204
+ return `${body}.${signature}`;
205
+ }
206
+ //# sourceMappingURL=google-service-account.token-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-service-account.token-source.js","sourceRoot":"","sources":["../../src/google-service-account/google-service-account.token-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { isGoogleServiceAccountAuth, findUnservedGoogleServiceAccountAuth, holdsLiteralGoogleServiceAccountKey, } from './google-service-account.auth.js';
2
+ export { GoogleAuthHttpProtocol, installGoogleServiceAccountAuth } from './google-auth-http.protocol.js';
3
+ export { GoogleServiceAccountTokenSource, GOOGLE_TOKEN_URL } from './google-service-account.token-source.js';
4
+ export { GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE, ServiceAccountAuthError, type GoogleServiceAccountAuth, type IServiceAccountTokenSource, } from './service-account-token.contract.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/google-service-account/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,0BAA0B,EAC1B,oCAAoC,EACpC,mCAAmC,GACpC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,MAAM,gCAAgC,CAAC;AACzG,OAAO,EAAE,+BAA+B,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC7G,OAAO,EACL,gCAAgC,EAChC,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,qCAAqC,CAAC"}
@@ -0,0 +1,7 @@
1
+ // Importing this module registers the `google_service_account` auth type with
2
+ // UTCP and puts the `http` protocol that acts on it in place of the stock one.
3
+ export { isGoogleServiceAccountAuth, findUnservedGoogleServiceAccountAuth, holdsLiteralGoogleServiceAccountKey, } from './google-service-account.auth.js';
4
+ export { GoogleAuthHttpProtocol, installGoogleServiceAccountAuth } from './google-auth-http.protocol.js';
5
+ export { GoogleServiceAccountTokenSource, GOOGLE_TOKEN_URL } from './google-service-account.token-source.js';
6
+ export { GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE, ServiceAccountAuthError, } from './service-account-token.contract.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/google-service-account/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,+EAA+E;AAC/E,OAAO,EACL,0BAA0B,EAC1B,oCAAoC,EACpC,mCAAmC,GACpC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,MAAM,gCAAgC,CAAC;AACzG,OAAO,EAAE,+BAA+B,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC7G,OAAO,EACL,gCAAgC,EAChC,uBAAuB,GAGxB,MAAM,qCAAqC,CAAC"}
@@ -0,0 +1,33 @@
1
+ /** The `auth_type` a `.tool` call template names to call a Google API as a service account. */
2
+ export declare const GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE = "google_service_account";
3
+ /**
4
+ * A call template's `auth` block for a Google service account, as it reaches
5
+ * the protocol: variables already substituted, so `credentials` holds the key
6
+ * itself rather than the `${VAR}` that named it.
7
+ */
8
+ export interface GoogleServiceAccountAuth {
9
+ auth_type: typeof GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE;
10
+ /** The service-account key JSON Google issued, or that JSON in base64. Normally `${VAR}`, filled from the Secrets Vault. */
11
+ credentials: string;
12
+ /** The OAuth scopes the token is for: one scope, a space-separated list, or an array. */
13
+ scopes: string | string[];
14
+ /** The user to act as under domain-wide delegation. Absent, the token is the service account's own. */
15
+ subject?: string;
16
+ }
17
+ /**
18
+ * Answers the one question a tool call has of a service account: which bearer
19
+ * token to send right now. How the token is minted and how long it is kept is
20
+ * the implementation's business.
21
+ */
22
+ export interface IServiceAccountTokenSource {
23
+ accessToken(auth: GoogleServiceAccountAuth): Promise<string>;
24
+ }
25
+ /** A service-account token could not be had. The message never carries the key. */
26
+ export declare class ServiceAccountAuthError extends Error {
27
+ /** How long the same token should not be asked for again. */
28
+ readonly retryAfterMs: number;
29
+ constructor(message: string,
30
+ /** How long the same token should not be asked for again. */
31
+ retryAfterMs?: number);
32
+ }
33
+ //# sourceMappingURL=service-account-token.contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-account-token.contract.d.ts","sourceRoot":"","sources":["../../src/google-service-account/service-account-token.contract.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,eAAO,MAAM,gCAAgC,2BAA2B,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,OAAO,gCAAgC,CAAC;IACnD,4HAA4H;IAC5H,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,uGAAuG;IACvG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9D;AAED,mFAAmF;AACnF,qBAAa,uBAAwB,SAAQ,KAAK;IAG9C,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,MAAM;gBAF7B,OAAO,EAAE,MAAM;IACf,6DAA6D;IACpD,YAAY,GAAE,MAAc;CAKxC"}
@@ -0,0 +1,14 @@
1
+ /** The `auth_type` a `.tool` call template names to call a Google API as a service account. */
2
+ export const GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE = 'google_service_account';
3
+ /** A service-account token could not be had. The message never carries the key. */
4
+ export class ServiceAccountAuthError extends Error {
5
+ retryAfterMs;
6
+ constructor(message,
7
+ /** How long the same token should not be asked for again. */
8
+ retryAfterMs = 5_000) {
9
+ super(message);
10
+ this.retryAfterMs = retryAfterMs;
11
+ this.name = 'ServiceAccountAuthError';
12
+ }
13
+ }
14
+ //# sourceMappingURL=service-account-token.contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-account-token.contract.js","sourceRoot":"","sources":["../../src/google-service-account/service-account-token.contract.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,CAAC,MAAM,gCAAgC,GAAG,wBAAwB,CAAC;AA0BzE,mFAAmF;AACnF,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAIrC;IAHX,YACE,OAAe;IACf,6DAA6D;IACpD,eAAuB,KAAK;QAErC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,iBAAY,GAAZ,YAAY,CAAgB;QAGrC,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF"}
package/dist/index.d.ts CHANGED
@@ -37,4 +37,5 @@ export { type SkillSummary, type LoadedSkill, skillPromptText, } from './skills.
37
37
  export { utcpNamespacePrefix, utcpNamespacedKey, seedBevelHostedManualVars, } from './utcp-namespace.js';
38
38
  export { sanitizeIdentifier, utcpNameToTsInterfaceName, findToolByName, findToolsByNames, AmbiguousToolNameError, } from './code-mode-names.js';
39
39
  export { printable } from './printable.js';
40
+ export { GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE, GOOGLE_TOKEN_URL, GoogleAuthHttpProtocol, GoogleServiceAccountTokenSource, ServiceAccountAuthError, findUnservedGoogleServiceAccountAuth, holdsLiteralGoogleServiceAccountKey, installGoogleServiceAccountAuth, isGoogleServiceAccountAuth, type GoogleServiceAccountAuth, type IServiceAccountTokenSource, } from './google-service-account/index.js';
40
41
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EACL,KAAK,WAAW,EAChB,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,wBAAwB,EACxB,qBAAqB,EACrB,KAAK,cAAc,EACnB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,SAAS,EACd,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EACL,KAAK,WAAW,EAChB,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,wBAAwB,EACxB,qBAAqB,EACrB,KAAK,cAAc,EACnB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,SAAS,EACd,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAM3C,OAAO,EACL,gCAAgC,EAChC,gBAAgB,EAChB,sBAAsB,EACtB,+BAA+B,EAC/B,uBAAuB,EACvB,oCAAoC,EACpC,mCAAmC,EACnC,+BAA+B,EAC/B,0BAA0B,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,mCAAmC,CAAC"}
package/dist/index.js CHANGED
@@ -37,4 +37,9 @@ export { skillPromptText, } from './skills.js';
37
37
  export { utcpNamespacePrefix, utcpNamespacedKey, seedBevelHostedManualVars, } from './utcp-namespace.js';
38
38
  export { sanitizeIdentifier, utcpNameToTsInterfaceName, findToolByName, findToolsByNames, AmbiguousToolNameError, } from './code-mode-names.js';
39
39
  export { printable } from './printable.js';
40
+ // Loading this package teaches UTCP `auth_type: google_service_account`, on
41
+ // both surfaces alike: the auth type validates and the `http` protocol mints
42
+ // the token. A side effect of the import, like the `@utcp/http` registration
43
+ // it builds on, so neither surface has a step to forget.
44
+ export { GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE, GOOGLE_TOKEN_URL, GoogleAuthHttpProtocol, GoogleServiceAccountTokenSource, ServiceAccountAuthError, findUnservedGoogleServiceAccountAuth, holdsLiteralGoogleServiceAccountKey, installGoogleServiceAccountAuth, isGoogleServiceAccountAuth, } from './google-service-account/index.js';
40
45
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAEL,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,wBAAwB,EACxB,qBAAqB,EAErB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EAGxB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,EAGpB,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAGL,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,sBAAsB,GAEvB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAGL,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAEL,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,wBAAwB,EACxB,qBAAqB,EAErB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EAGxB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,EAGpB,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAGL,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,sBAAsB,GAEvB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAGL,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,yDAAyD;AACzD,OAAO,EACL,gCAAgC,EAChC,gBAAgB,EAChB,sBAAsB,EACtB,+BAA+B,EAC/B,uBAAuB,EACvB,oCAAoC,EACpC,mCAAmC,EACnC,+BAA+B,EAC/B,0BAA0B,GAG3B,MAAM,mCAAmC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bevel-software/platform-mcp-core",
3
- "version": "0.24.0",
3
+ "version": "0.25.2",
4
4
  "description": "The transport-agnostic half of Bevel's MCP surface: UTCP manual registration, tool discovery, MCP listing/dispatch and the code-mode meta-tools. Shared by the hosted proxy and the local stdio server.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,7 +27,8 @@
27
27
  "@utcp/code-mode": "^1.2.13",
28
28
  "@utcp/http": "^1.1.12",
29
29
  "@utcp/mcp": "^1.2.0",
30
- "@utcp/sdk": "^1.2.0"
30
+ "@utcp/sdk": "^1.2.0",
31
+ "zod": "^4.0.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "typescript": "^5.8.0",
@@ -0,0 +1,62 @@
1
+ import { CommunicationProtocol, type CallTemplate, type IUtcpClient } from '@utcp/sdk';
2
+ import { HttpCommunicationProtocol, type HttpCallTemplate } from '@utcp/http';
3
+ import { isGoogleServiceAccountAuth } from './google-service-account.auth.js';
4
+ import { GoogleServiceAccountTokenSource } from './google-service-account.token-source.js';
5
+ import type { IServiceAccountTokenSource } from './service-account-token.contract.js';
6
+
7
+ /**
8
+ * The stock UTCP `http` protocol, taught one more auth type. A call whose
9
+ * template names `google_service_account` is handed to the stock protocol with
10
+ * that auth swapped for the bearer token it resolves to (as `oauth2_user`,
11
+ * which the stock protocol sends as `Authorization: Bearer <token>` and strips
12
+ * on a cross-origin redirect). Every other call passes through untouched.
13
+ */
14
+ export class GoogleAuthHttpProtocol extends HttpCommunicationProtocol {
15
+ constructor(private readonly tokens: IServiceAccountTokenSource) {
16
+ super();
17
+ }
18
+
19
+ override async callTool(
20
+ caller: IUtcpClient,
21
+ toolName: string,
22
+ toolArgs: Record<string, unknown>,
23
+ toolCallTemplate: CallTemplate,
24
+ ): Promise<unknown> {
25
+ return super.callTool(caller, toolName, toolArgs, await this.withBearerToken(toolCallTemplate));
26
+ }
27
+
28
+ override async *callToolStreaming(
29
+ caller: IUtcpClient,
30
+ toolName: string,
31
+ toolArgs: Record<string, unknown>,
32
+ toolCallTemplate: CallTemplate,
33
+ ): AsyncGenerator<unknown, void, unknown> {
34
+ yield* super.callToolStreaming(caller, toolName, toolArgs, await this.withBearerToken(toolCallTemplate));
35
+ }
36
+
37
+ private async withBearerToken(template: CallTemplate): Promise<CallTemplate> {
38
+ const auth = (template as HttpCallTemplate).auth;
39
+ if (!isGoogleServiceAccountAuth(auth)) return template;
40
+ const accessToken = await this.tokens.accessToken(auth);
41
+ return { ...template, auth: { auth_type: 'oauth2_user', access_token: accessToken } } as CallTemplate;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Put the service-account-aware protocol in place of the stock `http` one,
47
+ * minting tokens from `tokens`. Exported so a suite can answer for Google;
48
+ * a process never needs to call it, since loading this module already has.
49
+ */
50
+ export function installGoogleServiceAccountAuth(
51
+ tokens: IServiceAccountTokenSource = new GoogleServiceAccountTokenSource(),
52
+ ): void {
53
+ CommunicationProtocol.communicationProtocols['http'] = new GoogleAuthHttpProtocol(tokens);
54
+ }
55
+
56
+ // Installed on module load, the way `@utcp/http` installs the protocol this
57
+ // one replaces: once per process, before any client exists (a client copies
58
+ // the registry when it is built). The import of `@utcp/http` above has already
59
+ // run its own registration by the time this line does, whatever order the
60
+ // importing module lists the two in. One token cache then serves the process;
61
+ // its entries are keyed by the key itself, so knowledge bases never share one.
62
+ installGoogleServiceAccountAuth();
@@ -0,0 +1,161 @@
1
+ import { AuthSerializer, Serializer, type Auth } from '@utcp/sdk';
2
+ import { z } from 'zod';
3
+ import { GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE, type GoogleServiceAccountAuth } from './service-account-token.contract.js';
4
+
5
+ const GoogleServiceAccountAuthSchema = z.object({
6
+ auth_type: z.literal(GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE),
7
+ credentials: z
8
+ .string()
9
+ .min(1)
10
+ .describe('The service-account key JSON. Recommended to use a vault variable like "${GOOGLE_SA_KEY}".'),
11
+ // Trimmed before the length check, so a blank scope is refused here rather
12
+ // than reaching Google as an empty one.
13
+ scopes: z
14
+ .union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)])
15
+ .describe('OAuth scopes for the token, e.g. "https://www.googleapis.com/auth/adwords".'),
16
+ subject: z.string().trim().min(1).optional().describe('User to impersonate under domain-wide delegation.'),
17
+ });
18
+
19
+ class GoogleServiceAccountAuthSerializer extends Serializer<Auth> {
20
+ toDict(obj: Auth): Record<string, unknown> {
21
+ return { ...obj };
22
+ }
23
+
24
+ validateDict(obj: Record<string, unknown>): Auth {
25
+ return GoogleServiceAccountAuthSchema.parse(obj) as Auth;
26
+ }
27
+ }
28
+
29
+ /** Whether a call template's `auth` asks for a Google service-account token. */
30
+ export function isGoogleServiceAccountAuth(auth: unknown): auth is GoogleServiceAccountAuth {
31
+ return (
32
+ typeof auth === 'object' &&
33
+ auth !== null &&
34
+ (auth as { auth_type?: unknown }).auth_type === GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE
35
+ );
36
+ }
37
+
38
+ /** The one call template type whose protocol turns the key into a token. */
39
+ const SERVED_CALL_TEMPLATE_TYPE = 'http';
40
+
41
+ /**
42
+ * The other call template types an author is likely to have reached for, which
43
+ * the answer below may name. A type outside this list is the file's own text,
44
+ * and is described rather than quoted back.
45
+ */
46
+ const NAMEABLE_CALL_TEMPLATE_TYPES = ['sse', 'streamable_http', 'mcp', 'cli'];
47
+
48
+ /**
49
+ * Where in `doc` a `google_service_account` auth block sits that nothing will
50
+ * act on, as a phrase for a refusal, or null when every block is the `auth` of
51
+ * one of `toolCallTemplates` and that template is an `http` one.
52
+ *
53
+ * `toolCallTemplates` are the templates tools are really called through, which
54
+ * only the caller knows: the document's shape is its business. A block
55
+ * anywhere else is read by nothing, whatever the object around it looks like:
56
+ * a template the document never registers, an `auth_tools`, a block at the
57
+ * root of a file that discovers its tools from a url.
58
+ *
59
+ * UTCP validates an auth type on any call template that takes an `auth`, but
60
+ * only the `http` protocol mints the token. Every other protocol sends an auth
61
+ * type it does not know as no credentials at all, so such a tool would save
62
+ * cleanly and then call Google unauthenticated.
63
+ *
64
+ * Every block in the document is found, at any depth (see
65
+ * `googleServiceAccountBlocks`), and the first one in document order that
66
+ * nothing will act on is the one named.
67
+ */
68
+ export function findUnservedGoogleServiceAccountAuth(doc: unknown, toolCallTemplates: readonly unknown[]): string | null {
69
+ const served = new Set<unknown>(toolCallTemplates);
70
+ for (const { parent, key } of googleServiceAccountBlocks(doc)) {
71
+ // Judged by where it sits, so once per place it appears: an aliased
72
+ // block can be served in one place and ignored in another.
73
+ const where = unservedPlacement(parent, key, served);
74
+ if (where) return where;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /** `${NAME}` or `$NAME`, as UTCP spells a variable, and nothing else around it. */
80
+ const ONE_VARIABLE_REFERENCE = /^\s*(?:\$\{[a-zA-Z0-9_]+\}|\$[a-zA-Z0-9_]+)\s*$/;
81
+
82
+ /**
83
+ * Whether any `google_service_account` block in `doc` has its key WRITTEN IN
84
+ * THE DOCUMENT: a `credentials` that is anything but one variable reference.
85
+ *
86
+ * The document is a `.tool`, which is knowledge-base content: it is committed
87
+ * to the repository, and read by everyone and every agent that can read the
88
+ * knowledge base. A key written into it is a key all of them hold, for as
89
+ * long as the history keeps it. So the place for the key is the vault, and
90
+ * `credentials` only ever names the variable.
91
+ *
92
+ * Asked of the document as its author wrote it, wherever the block sits: a
93
+ * key in a block nothing will act on is just as readable. It cannot be asked
94
+ * where the auth type itself is validated, because that also runs on the
95
+ * template AFTER its variables were substituted, where `credentials` is the
96
+ * key and has to be.
97
+ *
98
+ * Says only that one was found. It never returns, quotes or measures the
99
+ * value: what it would be describing is the secret.
100
+ */
101
+ export function holdsLiteralGoogleServiceAccountKey(doc: unknown): boolean {
102
+ for (const { block } of googleServiceAccountBlocks(doc)) {
103
+ const { credentials } = block as { credentials?: unknown };
104
+ // Not a string is not a key either; the auth type's own schema refuses it.
105
+ if (typeof credentials === 'string' && !ONE_VARIABLE_REFERENCE.test(credentials)) return true;
106
+ }
107
+ return false;
108
+ }
109
+
110
+ /**
111
+ * Every `google_service_account` auth block in `doc`, at any depth, with the
112
+ * object it sits in and the key it sits under, in document order.
113
+ *
114
+ * The walk keeps its own stack, so a deeply nested document cannot overflow
115
+ * the call stack, and it does not re-enter an object it has seen: a YAML
116
+ * anchor aliased inside itself parses to a cyclic object. A block is yielded
117
+ * once per place it appears, since an aliased one sits in several.
118
+ */
119
+ function* googleServiceAccountBlocks(
120
+ doc: unknown,
121
+ ): Generator<{ block: GoogleServiceAccountAuth; parent?: Record<string, unknown>; key?: string }> {
122
+ const seen = new WeakSet<object>();
123
+ const pending: { node: unknown; parent?: Record<string, unknown>; key?: string }[] = [{ node: doc }];
124
+ for (let next = pending.pop(); next; next = pending.pop()) {
125
+ const { node, parent, key } = next;
126
+ if (!node || typeof node !== 'object') continue;
127
+ if (isGoogleServiceAccountAuth(node)) {
128
+ yield { block: node, parent, key };
129
+ continue;
130
+ }
131
+ if (seen.has(node)) continue;
132
+ seen.add(node);
133
+ // Pushed in reverse, so blocks come out in document order.
134
+ if (Array.isArray(node)) {
135
+ for (let i = node.length - 1; i >= 0; i--) pending.push({ node: node[i] });
136
+ } else {
137
+ const obj = node as Record<string, unknown>;
138
+ const keys = Object.keys(obj);
139
+ for (let i = keys.length - 1; i >= 0; i--) pending.push({ node: obj[keys[i]!], parent: obj, key: keys[i] });
140
+ }
141
+ }
142
+ }
143
+
144
+ /** Why a block under `parent[key]` is acted on by nothing, or null when it is. */
145
+ function unservedPlacement(parent: Record<string, unknown> | undefined, key: string | undefined, served: Set<unknown>): string | null {
146
+ if (!parent || key !== 'auth' || typeof parent.call_template_type !== 'string') {
147
+ return "somewhere that is not a call template's `auth`";
148
+ }
149
+ const type = parent.call_template_type.toLowerCase().trim();
150
+ if (type !== SERVED_CALL_TEMPLATE_TYPE) {
151
+ return NAMEABLE_CALL_TEMPLATE_TYPES.includes(type) ? `a \`${type}\` call template` : 'a call template that is not an `http` one';
152
+ }
153
+ return served.has(parent) ? null : 'an `http` call template that no tool is called through';
154
+ }
155
+
156
+ // Register the auth type on module load, so a `.tool` naming it validates
157
+ // wherever UTCP parses a call template: the inline manual route, the preview,
158
+ // and the client re-validating a template after substituting its variables.
159
+ // UTCP's registry is process-wide and the type carries no state, so one
160
+ // registration serves every knowledge base. Idempotent (safe under hot-reload).
161
+ AuthSerializer.registerAuth(GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE, new GoogleServiceAccountAuthSerializer(), true);
@@ -0,0 +1,239 @@
1
+ import { createHash, createSign } from 'node:crypto';
2
+ import {
3
+ ServiceAccountAuthError,
4
+ type GoogleServiceAccountAuth,
5
+ type IServiceAccountTokenSource,
6
+ } from './service-account-token.contract.js';
7
+
8
+ /**
9
+ * Google's token endpoint. Fixed rather than read from the key's own
10
+ * `token_uri`, so a key cannot point the server at another host with a signed
11
+ * assertion in hand.
12
+ */
13
+ export const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
14
+ const JWT_BEARER_GRANT = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
15
+ /** The longest assertion lifetime Google accepts. */
16
+ const ASSERTION_LIFETIME_S = 3600;
17
+ /** A token this close to expiry is replaced, so a call never leaves with one that dies on the way. */
18
+ const REFRESH_SKEW_MS = 60_000;
19
+ const REQUEST_TIMEOUT_MS = 10_000;
20
+ /** Tokens (and failures) kept at once. Each distinct key, scope set and subject is one entry. */
21
+ const MAX_CACHED_TOKENS = 256;
22
+ /** How long a failed exchange is answered from memory when Google names no `Retry-After`. */
23
+ const FAILURE_BACKOFF_MS = 5_000;
24
+ /** The longest a `Retry-After` from Google holds calls back, so a far-off value cannot park a tool for good. */
25
+ const MAX_FAILURE_BACKOFF_MS = 60_000;
26
+
27
+ interface ServiceAccountKey {
28
+ clientEmail: string;
29
+ privateKey: string;
30
+ privateKeyId?: string;
31
+ }
32
+
33
+ interface CachedToken {
34
+ accessToken: string;
35
+ expiresAt: number;
36
+ }
37
+
38
+ interface CachedFailure {
39
+ error: ServiceAccountAuthError;
40
+ retryAt: number;
41
+ }
42
+
43
+ /**
44
+ * Mints and caches access tokens for Google service accounts: signs an RS256
45
+ * assertion with the key's private key, exchanges it at Google's token
46
+ * endpoint, and keeps the token until shortly before it expires. Concurrent
47
+ * calls for the same token share one exchange, and a failed exchange is
48
+ * answered from memory for a few seconds (or for Google's `Retry-After`), so
49
+ * an outage or a rate limit is not met with a fresh exchange on every call.
50
+ * Injected with `fetch` and a clock so suites answer for Google.
51
+ */
52
+ export class GoogleServiceAccountTokenSource implements IServiceAccountTokenSource {
53
+ private readonly tokens = new Map<string, CachedToken>();
54
+ private readonly failures = new Map<string, CachedFailure>();
55
+ private readonly inFlight = new Map<string, Promise<string>>();
56
+
57
+ constructor(
58
+ private readonly fetchImpl: typeof fetch = fetch,
59
+ private readonly now: () => number = Date.now,
60
+ private readonly timeoutMs: number = REQUEST_TIMEOUT_MS,
61
+ ) {}
62
+
63
+ async accessToken(auth: GoogleServiceAccountAuth): Promise<string> {
64
+ const key = parseServiceAccountKey(auth.credentials);
65
+ const scope = scopeString(auth.scopes);
66
+ const cacheKey = tokenCacheKey(key, scope, auth.subject);
67
+
68
+ const cached = this.tokens.get(cacheKey);
69
+ if (cached && cached.expiresAt - REFRESH_SKEW_MS > this.now()) return cached.accessToken;
70
+
71
+ const failed = this.failures.get(cacheKey);
72
+ if (failed && failed.retryAt > this.now()) throw failed.error;
73
+
74
+ const pending = this.inFlight.get(cacheKey);
75
+ if (pending) return pending;
76
+
77
+ const exchange = this.exchange(key, scope, auth.subject, cacheKey)
78
+ .catch((err: unknown) => {
79
+ // Remembered per key, scope set and subject: a corrected key hashes to
80
+ // another entry, so fixing it is never held back by this.
81
+ if (err instanceof ServiceAccountAuthError) {
82
+ remember(this.failures, cacheKey, { error: err, retryAt: this.now() + err.retryAfterMs });
83
+ }
84
+ throw err;
85
+ })
86
+ .finally(() => this.inFlight.delete(cacheKey));
87
+ this.inFlight.set(cacheKey, exchange);
88
+ return exchange;
89
+ }
90
+
91
+ private async exchange(key: ServiceAccountKey, scope: string, subject: string | undefined, cacheKey: string): Promise<string> {
92
+ const assertion = signAssertion(key, scope, subject, this.now());
93
+ let res: Response;
94
+ try {
95
+ res = await this.fetchImpl(GOOGLE_TOKEN_URL, {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
98
+ body: new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion }).toString(),
99
+ // Don't follow redirects: the assertion goes to Google's endpoint and nowhere else.
100
+ redirect: 'error',
101
+ signal: AbortSignal.timeout(this.timeoutMs),
102
+ });
103
+ } catch (err) {
104
+ const reason =
105
+ err instanceof Error && err.name === 'TimeoutError'
106
+ ? `timed out after ${this.timeoutMs}ms`
107
+ : err instanceof Error
108
+ ? err.message
109
+ : String(err);
110
+ throw new ServiceAccountAuthError(`Google's token endpoint could not be reached: ${reason}`);
111
+ }
112
+
113
+ // Whatever came back is read as an object of fields or as none. A body
114
+ // that is not JSON, and one that is JSON but no object (`null`, a number,
115
+ // a list), both carry no field to read: reading one off `null` would
116
+ // throw a TypeError instead of the refusal below, past the code that
117
+ // remembers a failure and says who was refused.
118
+ const parsed: unknown = await res.json().catch(() => null);
119
+ const body = (parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}) as Record<string, unknown>;
120
+ if (!res.ok) {
121
+ // Google says why in `error` and `error_description` (a revoked key, a
122
+ // scope the account may not have, a subject without delegation); neither
123
+ // carries the key, so both are passed on.
124
+ const detail = [body.error, body.error_description].filter((v) => typeof v === 'string').join(': ');
125
+ throw new ServiceAccountAuthError(
126
+ `Google refused the service account ${key.clientEmail} (HTTP ${res.status})${detail ? `: ${detail}` : ''}`,
127
+ retryAfterMs(res.headers.get('retry-after'), this.now()),
128
+ );
129
+ }
130
+ const accessToken = typeof body.access_token === 'string' ? body.access_token : '';
131
+ if (!accessToken) throw new ServiceAccountAuthError("Google's token response had no access_token");
132
+ const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : ASSERTION_LIFETIME_S;
133
+
134
+ this.failures.delete(cacheKey);
135
+ remember(this.tokens, cacheKey, { accessToken, expiresAt: this.now() + expiresIn * 1000 });
136
+ return accessToken;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Keep `value` under `cacheKey`, dropping the oldest entry first when the map
142
+ * is full: a Map iterates in insertion order, and re-inserting moves an entry
143
+ * to the back.
144
+ */
145
+ function remember<T>(map: Map<string, T>, cacheKey: string, value: T): void {
146
+ map.delete(cacheKey);
147
+ if (map.size >= MAX_CACHED_TOKENS) {
148
+ const oldest = map.keys().next().value;
149
+ if (oldest !== undefined) map.delete(oldest);
150
+ }
151
+ map.set(cacheKey, value);
152
+ }
153
+
154
+ /**
155
+ * How long to hold calls back after a refusal: Google's `Retry-After` (seconds
156
+ * or an HTTP date) when it sent one, capped, else the default backoff.
157
+ */
158
+ function retryAfterMs(header: string | null, now: number): number {
159
+ if (!header) return FAILURE_BACKOFF_MS;
160
+ const seconds = Number(header);
161
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(header) - now;
162
+ if (!Number.isFinite(ms) || ms <= 0) return FAILURE_BACKOFF_MS;
163
+ return Math.min(ms, MAX_FAILURE_BACKOFF_MS);
164
+ }
165
+
166
+ /**
167
+ * The key as an admin is likely to have stored it: the JSON file Google
168
+ * issued, or that JSON in base64. Only the fields the assertion needs are
169
+ * kept, and no error quotes what was stored.
170
+ */
171
+ function parseServiceAccountKey(raw: string): ServiceAccountKey {
172
+ const value = raw.trim();
173
+ let parsed: unknown;
174
+ try {
175
+ parsed = JSON.parse(value.startsWith('{') ? value : Buffer.from(value, 'base64').toString('utf8'));
176
+ } catch {
177
+ throw new ServiceAccountAuthError(
178
+ 'The service-account credentials are not a key JSON. Store the JSON file Google issued for the service account.',
179
+ );
180
+ }
181
+ const obj = (parsed ?? {}) as Record<string, unknown>;
182
+ const clientEmail = typeof obj.client_email === 'string' ? obj.client_email : '';
183
+ const privateKey = typeof obj.private_key === 'string' ? obj.private_key : '';
184
+ if (!clientEmail || !privateKey) {
185
+ throw new ServiceAccountAuthError(
186
+ 'The service-account key JSON has no client_email or private_key. Store the JSON file Google issued for the service account.',
187
+ );
188
+ }
189
+ return {
190
+ clientEmail,
191
+ privateKey,
192
+ privateKeyId: typeof obj.private_key_id === 'string' ? obj.private_key_id : undefined,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * The scopes as Google wants them: space-separated, each trimmed. UTCP keeps
198
+ * the template as written once it validates, so stray whitespace is cleaned
199
+ * here, whether it sits in a list entry or in a space-separated string.
200
+ */
201
+ function scopeString(scopes: string | string[]): string {
202
+ const list = Array.isArray(scopes) ? scopes : [scopes];
203
+ return list
204
+ .flatMap((s) => s.split(/\s+/))
205
+ .filter(Boolean)
206
+ .join(' ');
207
+ }
208
+
209
+ /** One entry per key, scope set and subject. The private key is hashed so the cache holds no copy of it. */
210
+ function tokenCacheKey(key: ServiceAccountKey, scope: string, subject: string | undefined): string {
211
+ const keyHash = createHash('sha256').update(key.privateKey).digest('hex');
212
+ return JSON.stringify([key.clientEmail, key.privateKeyId ?? '', keyHash, scope.split(' ').sort().join(' '), subject ?? '']);
213
+ }
214
+
215
+ /** The RS256 assertion Google exchanges for an access token. */
216
+ function signAssertion(key: ServiceAccountKey, scope: string, subject: string | undefined, now: number): string {
217
+ const seconds = Math.floor(now / 1000);
218
+ const part = (value: unknown) => Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
219
+ const header = { alg: 'RS256', typ: 'JWT', ...(key.privateKeyId ? { kid: key.privateKeyId } : {}) };
220
+ const claims = {
221
+ iss: key.clientEmail,
222
+ scope,
223
+ aud: GOOGLE_TOKEN_URL,
224
+ iat: seconds,
225
+ exp: seconds + ASSERTION_LIFETIME_S,
226
+ ...(subject ? { sub: subject } : {}),
227
+ };
228
+ const body = `${part(header)}.${part(claims)}`;
229
+ let signature: string;
230
+ try {
231
+ signature = createSign('RSA-SHA256').update(body).end().sign(key.privateKey, 'base64url');
232
+ } catch {
233
+ // What the key was is never quoted.
234
+ throw new ServiceAccountAuthError(
235
+ `The private key of service account ${key.clientEmail} could not be read. It must be the PEM in the key JSON Google issued.`,
236
+ );
237
+ }
238
+ return `${body}.${signature}`;
239
+ }
@@ -0,0 +1,15 @@
1
+ // Importing this module registers the `google_service_account` auth type with
2
+ // UTCP and puts the `http` protocol that acts on it in place of the stock one.
3
+ export {
4
+ isGoogleServiceAccountAuth,
5
+ findUnservedGoogleServiceAccountAuth,
6
+ holdsLiteralGoogleServiceAccountKey,
7
+ } from './google-service-account.auth.js';
8
+ export { GoogleAuthHttpProtocol, installGoogleServiceAccountAuth } from './google-auth-http.protocol.js';
9
+ export { GoogleServiceAccountTokenSource, GOOGLE_TOKEN_URL } from './google-service-account.token-source.js';
10
+ export {
11
+ GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE,
12
+ ServiceAccountAuthError,
13
+ type GoogleServiceAccountAuth,
14
+ type IServiceAccountTokenSource,
15
+ } from './service-account-token.contract.js';
@@ -0,0 +1,38 @@
1
+ /** The `auth_type` a `.tool` call template names to call a Google API as a service account. */
2
+ export const GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE = 'google_service_account';
3
+
4
+ /**
5
+ * A call template's `auth` block for a Google service account, as it reaches
6
+ * the protocol: variables already substituted, so `credentials` holds the key
7
+ * itself rather than the `${VAR}` that named it.
8
+ */
9
+ export interface GoogleServiceAccountAuth {
10
+ auth_type: typeof GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE;
11
+ /** The service-account key JSON Google issued, or that JSON in base64. Normally `${VAR}`, filled from the Secrets Vault. */
12
+ credentials: string;
13
+ /** The OAuth scopes the token is for: one scope, a space-separated list, or an array. */
14
+ scopes: string | string[];
15
+ /** The user to act as under domain-wide delegation. Absent, the token is the service account's own. */
16
+ subject?: string;
17
+ }
18
+
19
+ /**
20
+ * Answers the one question a tool call has of a service account: which bearer
21
+ * token to send right now. How the token is minted and how long it is kept is
22
+ * the implementation's business.
23
+ */
24
+ export interface IServiceAccountTokenSource {
25
+ accessToken(auth: GoogleServiceAccountAuth): Promise<string>;
26
+ }
27
+
28
+ /** A service-account token could not be had. The message never carries the key. */
29
+ export class ServiceAccountAuthError extends Error {
30
+ constructor(
31
+ message: string,
32
+ /** How long the same token should not be asked for again. */
33
+ readonly retryAfterMs: number = 5_000,
34
+ ) {
35
+ super(message);
36
+ this.name = 'ServiceAccountAuthError';
37
+ }
38
+ }
package/src/index.ts CHANGED
@@ -120,3 +120,21 @@ export {
120
120
  } from './code-mode-names.js';
121
121
 
122
122
  export { printable } from './printable.js';
123
+
124
+ // Loading this package teaches UTCP `auth_type: google_service_account`, on
125
+ // both surfaces alike: the auth type validates and the `http` protocol mints
126
+ // the token. A side effect of the import, like the `@utcp/http` registration
127
+ // it builds on, so neither surface has a step to forget.
128
+ export {
129
+ GOOGLE_SERVICE_ACCOUNT_AUTH_TYPE,
130
+ GOOGLE_TOKEN_URL,
131
+ GoogleAuthHttpProtocol,
132
+ GoogleServiceAccountTokenSource,
133
+ ServiceAccountAuthError,
134
+ findUnservedGoogleServiceAccountAuth,
135
+ holdsLiteralGoogleServiceAccountKey,
136
+ installGoogleServiceAccountAuth,
137
+ isGoogleServiceAccountAuth,
138
+ type GoogleServiceAccountAuth,
139
+ type IServiceAccountTokenSource,
140
+ } from './google-service-account/index.js';