@webpieces/gcp-identity 0.3.353 → 0.3.354

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/gcp-identity",
3
- "version": "0.3.353",
3
+ "version": "0.3.354",
4
4
  "description": "GCP runtime identity: project/region metadata, Cloud Run URLs, OIDC mint/verify",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -23,8 +23,11 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@webpieces/core-util": "0.3.353",
26
+ "@webpieces/core-context": "0.3.354",
27
+ "@webpieces/core-util": "0.3.354",
27
28
  "google-auth-library": "9.15.1",
28
- "gcp-metadata": "6.1.1"
29
+ "gcp-metadata": "6.1.1",
30
+ "inversify": "7.10.4",
31
+ "reflect-metadata": "0.2.2"
29
32
  }
30
33
  }
package/src/index.d.ts CHANGED
@@ -10,4 +10,4 @@
10
10
  */
11
11
  export { isOnGcp } from './metadata';
12
12
  export { getServiceName, getProjectId, getRegion, getRuntimeServiceAccountEmail, getSelfCloudRunUrl, getCloudRunUrl, resolveTargetUrl, LOCAL_SERVICE_ACCOUNT_EMAIL, } from './urls';
13
- export { mintIdToken, verifyOidcFromCallers, OidcVerifyResult } from './oidc';
13
+ export { GcpOidc, OidcVerifyResult } from './oidc';
package/src/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * via an app-bound AuthConfig) and the RPC + Cloud Tasks clients.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.OidcVerifyResult = exports.verifyOidcFromCallers = exports.mintIdToken = exports.LOCAL_SERVICE_ACCOUNT_EMAIL = exports.resolveTargetUrl = exports.getCloudRunUrl = exports.getSelfCloudRunUrl = exports.getRuntimeServiceAccountEmail = exports.getRegion = exports.getProjectId = exports.getServiceName = exports.isOnGcp = void 0;
13
+ exports.OidcVerifyResult = exports.GcpOidc = exports.LOCAL_SERVICE_ACCOUNT_EMAIL = exports.resolveTargetUrl = exports.getCloudRunUrl = exports.getSelfCloudRunUrl = exports.getRuntimeServiceAccountEmail = exports.getRegion = exports.getProjectId = exports.getServiceName = exports.isOnGcp = void 0;
14
14
  var metadata_1 = require("./metadata");
15
15
  Object.defineProperty(exports, "isOnGcp", { enumerable: true, get: function () { return metadata_1.isOnGcp; } });
16
16
  var urls_1 = require("./urls");
@@ -23,7 +23,6 @@ Object.defineProperty(exports, "getCloudRunUrl", { enumerable: true, get: functi
23
23
  Object.defineProperty(exports, "resolveTargetUrl", { enumerable: true, get: function () { return urls_1.resolveTargetUrl; } });
24
24
  Object.defineProperty(exports, "LOCAL_SERVICE_ACCOUNT_EMAIL", { enumerable: true, get: function () { return urls_1.LOCAL_SERVICE_ACCOUNT_EMAIL; } });
25
25
  var oidc_1 = require("./oidc");
26
- Object.defineProperty(exports, "mintIdToken", { enumerable: true, get: function () { return oidc_1.mintIdToken; } });
27
- Object.defineProperty(exports, "verifyOidcFromCallers", { enumerable: true, get: function () { return oidc_1.verifyOidcFromCallers; } });
26
+ Object.defineProperty(exports, "GcpOidc", { enumerable: true, get: function () { return oidc_1.GcpOidc; } });
28
27
  Object.defineProperty(exports, "OidcVerifyResult", { enumerable: true, get: function () { return oidc_1.OidcVerifyResult; } });
29
28
  //# sourceMappingURL=index.js.map
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAEH,uCAAqC;AAA5B,mGAAA,OAAO,OAAA;AAChB,+BASgB;AARZ,sGAAA,cAAc,OAAA;AACd,oGAAA,YAAY,OAAA;AACZ,iGAAA,SAAS,OAAA;AACT,qHAAA,6BAA6B,OAAA;AAC7B,0GAAA,kBAAkB,OAAA;AAClB,sGAAA,cAAc,OAAA;AACd,wGAAA,gBAAgB,OAAA;AAChB,mHAAA,2BAA2B,OAAA;AAE/B,+BAA8E;AAArE,mGAAA,WAAW,OAAA;AAAE,6GAAA,qBAAqB,OAAA;AAAE,wGAAA,gBAAgB,OAAA","sourcesContent":["/**\n * @webpieces/gcp-identity\n *\n * GCP runtime identity (Node-only). Metadata, Cloud Run URLs, and OIDC mint/verify,\n * all read from the metadata server / ADC at runtime with deterministic localhost\n * fallbacks off-GCP so local dev and tests never touch GCP.\n *\n * Underpins the @AuthOidc service-to-service auth mode (enforced by the framework AuthFilter\n * via an app-bound AuthConfig) and the RPC + Cloud Tasks clients.\n */\n\nexport { isOnGcp } from './metadata';\nexport {\n getServiceName,\n getProjectId,\n getRegion,\n getRuntimeServiceAccountEmail,\n getSelfCloudRunUrl,\n getCloudRunUrl,\n resolveTargetUrl,\n LOCAL_SERVICE_ACCOUNT_EMAIL,\n} from './urls';\nexport { mintIdToken, verifyOidcFromCallers, OidcVerifyResult } from './oidc';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAEH,uCAAqC;AAA5B,mGAAA,OAAO,OAAA;AAChB,+BASgB;AARZ,sGAAA,cAAc,OAAA;AACd,oGAAA,YAAY,OAAA;AACZ,iGAAA,SAAS,OAAA;AACT,qHAAA,6BAA6B,OAAA;AAC7B,0GAAA,kBAAkB,OAAA;AAClB,sGAAA,cAAc,OAAA;AACd,wGAAA,gBAAgB,OAAA;AAChB,mHAAA,2BAA2B,OAAA;AAE/B,+BAAmD;AAA1C,+FAAA,OAAO,OAAA;AAAE,wGAAA,gBAAgB,OAAA","sourcesContent":["/**\n * @webpieces/gcp-identity\n *\n * GCP runtime identity (Node-only). Metadata, Cloud Run URLs, and OIDC mint/verify,\n * all read from the metadata server / ADC at runtime with deterministic localhost\n * fallbacks off-GCP so local dev and tests never touch GCP.\n *\n * Underpins the @AuthOidc service-to-service auth mode (enforced by the framework AuthFilter\n * via an app-bound AuthConfig) and the RPC + Cloud Tasks clients.\n */\n\nexport { isOnGcp } from './metadata';\nexport {\n getServiceName,\n getProjectId,\n getRegion,\n getRuntimeServiceAccountEmail,\n getSelfCloudRunUrl,\n getCloudRunUrl,\n resolveTargetUrl,\n LOCAL_SERVICE_ACCOUNT_EMAIL,\n} from './urls';\nexport { GcpOidc, OidcVerifyResult } from './oidc';\n"]}
package/src/oidc.d.ts CHANGED
@@ -9,18 +9,55 @@ export declare class OidcVerifyResult {
9
9
  constructor(ok: boolean, email?: string, reason?: string);
10
10
  }
11
11
  /**
12
- * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as
13
- * this service's runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token
14
- * that verifyOidcFromCallers accepts locally.
15
- */
16
- export declare function mintIdToken(audience: string): Promise<string>;
17
- /**
18
- * Verify an inbound OIDC token and require its caller SA to be in the allow-list.
19
- * Audience is deliberately NOT gated (mirrors the platform's cross-service model —
20
- * the callee's own `aud` passes). Never throws — an invalid token or a disallowed
21
- * caller comes back as `ok:false` so callers map it to a 401 without a try/catch.
12
+ * GcpOidc - Google OIDC service-to-service tokens: MINT an outbound token as this service's runtime
13
+ * SA, and VERIFY an inbound token against a caller allow-list. Injected by type (@provideSingleton /
14
+ * inject-by-type) into the RPC + Cloud Tasks clients (mint) and the framework auth verifier (verify),
15
+ * so no DI-managed code calls gcp-identity as a free function.
22
16
  *
23
- * `callers` entries resolve as: 'self' this service's runtime SA; a bare id →
24
- * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' verbatim.
17
+ * Holds the reusable google-auth-library clients as instance state (one per process, since this is a
18
+ * framework singleton) plus the "warned about public posture once" flag.
25
19
  */
26
- export declare function verifyOidcFromCallers(idToken: string, callers: string[]): Promise<OidcVerifyResult>;
20
+ export declare class GcpOidc {
21
+ private readonly auth;
22
+ private readonly verifier;
23
+ private publicPostureChecked;
24
+ /**
25
+ * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as this service's
26
+ * runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token that {@link verifyFromCallers}
27
+ * accepts locally.
28
+ */
29
+ mintIdToken(audience: string): Promise<string>;
30
+ /**
31
+ * Verify an inbound OIDC token and require its caller SA to be in the allow-list. Audience is
32
+ * deliberately NOT gated (mirrors the platform's cross-service model — the callee's own `aud`
33
+ * passes). Never throws — an invalid token or a disallowed caller comes back as `ok:false` so
34
+ * callers map it to a 401 without a try/catch.
35
+ *
36
+ * `callers` entries resolve as: 'self' → this service's runtime SA; a bare id →
37
+ * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' → verbatim.
38
+ */
39
+ verifyFromCallers(idToken: string, callers: string[]): Promise<OidcVerifyResult>;
40
+ /**
41
+ * @AuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge
42
+ * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only
43
+ * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.
44
+ */
45
+ private warnIfPublicOnce;
46
+ /**
47
+ * True when THIS Cloud Run service grants run.invoker to allUsers/allAuthenticatedUsers (public).
48
+ * Reads the service's OWN IAM policy via the Run Admin API (needs run.services.getIamPolicy on the
49
+ * runtime SA). On any failure we cannot tell → false (no false alarm).
50
+ */
51
+ private isServicePublic;
52
+ private makeDevToken;
53
+ private extractVerifiedEmail;
54
+ /**
55
+ * True when an error thrown while verifying a token is INFRASTRUCTURE (network/system), not a bad
56
+ * token — e.g. the metadata/cert fetch failed. These must NOT be swallowed as a 401 (that would
57
+ * both mislead the caller and hide real outages/bugs behind a client-error status).
58
+ */
59
+ private isInfrastructureError;
60
+ private decodeDevTokenEmail;
61
+ private resolveCallers;
62
+ private resolveCaller;
63
+ }
package/src/oidc.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OidcVerifyResult = void 0;
4
- exports.mintIdToken = mintIdToken;
5
- exports.verifyOidcFromCallers = verifyOidcFromCallers;
3
+ exports.GcpOidc = exports.OidcVerifyResult = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const inversify_1 = require("inversify");
6
6
  const google_auth_library_1 = require("google-auth-library");
7
+ const core_context_1 = require("@webpieces/core-context");
7
8
  const core_util_1 = require("@webpieces/core-util");
8
9
  const core_util_2 = require("@webpieces/core-util");
9
10
  const metadata_1 = require("./metadata");
@@ -15,8 +16,6 @@ const log = core_util_1.LogManager.getLogger('gcp-identity-oidc');
15
16
  * the @AuthOidc code path fully exercised in tests without any GCP round-trip.
16
17
  */
17
18
  const DEV_TOKEN_PREFIX = 'dev-oidc.';
18
- const reusableAuth = new google_auth_library_1.GoogleAuth();
19
- const reusableVerifier = new google_auth_library_1.OAuth2Client();
20
19
  /** Outcome of verifying an inbound OIDC token against an allow-list of callers. */
21
20
  class OidcVerifyResult {
22
21
  /** True when the token is valid AND its caller SA is allowed. */
@@ -33,155 +32,173 @@ class OidcVerifyResult {
33
32
  }
34
33
  exports.OidcVerifyResult = OidcVerifyResult;
35
34
  /**
36
- * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as
37
- * this service's runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token
38
- * that verifyOidcFromCallers accepts locally.
39
- */
40
- async function mintIdToken(audience) {
41
- if (!(await (0, metadata_1.isOnGcp)())) {
42
- const email = await (0, urls_1.getRuntimeServiceAccountEmail)();
43
- return makeDevToken(email, audience);
44
- }
45
- const client = await reusableAuth.getIdTokenClient(audience);
46
- return client.idTokenProvider.fetchIdToken(audience);
47
- }
48
- /**
49
- * Verify an inbound OIDC token and require its caller SA to be in the allow-list.
50
- * Audience is deliberately NOT gated (mirrors the platform's cross-service model —
51
- * the callee's own `aud` passes). Never throws — an invalid token or a disallowed
52
- * caller comes back as `ok:false` so callers map it to a 401 without a try/catch.
35
+ * GcpOidc - Google OIDC service-to-service tokens: MINT an outbound token as this service's runtime
36
+ * SA, and VERIFY an inbound token against a caller allow-list. Injected by type (@provideSingleton /
37
+ * inject-by-type) into the RPC + Cloud Tasks clients (mint) and the framework auth verifier (verify),
38
+ * so no DI-managed code calls gcp-identity as a free function.
53
39
  *
54
- * `callers` entries resolve as: 'self' this service's runtime SA; a bare id →
55
- * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' verbatim.
40
+ * Holds the reusable google-auth-library clients as instance state (one per process, since this is a
41
+ * framework singleton) plus the "warned about public posture once" flag.
56
42
  */
57
- async function verifyOidcFromCallers(idToken, callers) {
58
- const email = await extractVerifiedEmail(idToken);
59
- if (!email) {
60
- return new OidcVerifyResult(false, undefined, 'token invalid or missing email claim');
61
- }
62
- // EMPTY allow-list = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a
63
- // PRIVATE Cloud Run service's edge already gated WHO via run.invoker IAM (managed in terraform,
64
- // one source of truth). Warn loudly (once) if the service is actually PUBLIC — then the edge is
65
- // NOT filtering callers and this is insecure. Non-empty list = explicit app-level allow-list.
66
- if (callers.length === 0) {
67
- await warnIfPublicOnce();
43
+ let GcpOidc = class GcpOidc {
44
+ auth = new google_auth_library_1.GoogleAuth();
45
+ verifier = new google_auth_library_1.OAuth2Client();
46
+ publicPostureChecked = false;
47
+ /**
48
+ * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as this service's
49
+ * runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token that {@link verifyFromCallers}
50
+ * accepts locally.
51
+ */
52
+ async mintIdToken(audience) {
53
+ if (!(await (0, metadata_1.isOnGcp)())) {
54
+ const email = await (0, urls_1.getRuntimeServiceAccountEmail)();
55
+ return this.makeDevToken(email, audience);
56
+ }
57
+ const client = await this.auth.getIdTokenClient(audience);
58
+ return client.idTokenProvider.fetchIdToken(audience);
59
+ }
60
+ /**
61
+ * Verify an inbound OIDC token and require its caller SA to be in the allow-list. Audience is
62
+ * deliberately NOT gated (mirrors the platform's cross-service model — the callee's own `aud`
63
+ * passes). Never throws — an invalid token or a disallowed caller comes back as `ok:false` so
64
+ * callers map it to a 401 without a try/catch.
65
+ *
66
+ * `callers` entries resolve as: 'self' → this service's runtime SA; a bare id →
67
+ * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' → verbatim.
68
+ */
69
+ async verifyFromCallers(idToken, callers) {
70
+ const email = await this.extractVerifiedEmail(idToken);
71
+ if (!email) {
72
+ return new OidcVerifyResult(false, undefined, 'token invalid or missing email claim');
73
+ }
74
+ // EMPTY allow-list = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a
75
+ // PRIVATE Cloud Run service's edge already gated WHO via run.invoker IAM (managed in terraform,
76
+ // one source of truth). Warn loudly (once) if the service is actually PUBLIC — then the edge is
77
+ // NOT filtering callers and this is insecure. Non-empty list = explicit app-level allow-list.
78
+ if (callers.length === 0) {
79
+ await this.warnIfPublicOnce();
80
+ return new OidcVerifyResult(true, email);
81
+ }
82
+ const allowed = await this.resolveCallers(callers);
83
+ if (!allowed.includes(email)) {
84
+ return new OidcVerifyResult(false, email, `caller '${email}' is not in the allow-list [${allowed.join(', ')}]`);
85
+ }
68
86
  return new OidcVerifyResult(true, email);
69
87
  }
70
- const allowed = await resolveCallers(callers);
71
- if (!allowed.includes(email)) {
72
- return new OidcVerifyResult(false, email, `caller '${email}' is not in the allow-list [${allowed.join(', ')}]`);
73
- }
74
- return new OidcVerifyResult(true, email);
75
- }
76
- let publicPostureChecked = false;
77
- /**
78
- * @AuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge
79
- * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only
80
- * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.
81
- */
82
- async function warnIfPublicOnce() {
83
- if (publicPostureChecked) {
84
- return;
85
- }
86
- publicPostureChecked = true; // attempt at most once, even if the check itself errors
87
- if (!(await (0, metadata_1.isOnGcp)())) {
88
- return; // off-GCP: there is no Cloud Run edge; public/private does not apply.
89
- }
90
- if (await isServicePublic()) {
91
- log.error('This endpoint is currently public but marked @AuthOidc which requires the service to be ' +
92
- 'private. You are currently running in a very insecure mode; however, we are only allowing ' +
93
- 'google-signed callers in. The edge will validate callers are allowed in (make the Cloud Run ' +
94
- 'service private — remove the allUsers run.invoker binding) to reduce your attack surface.');
95
- }
96
- }
97
- /**
98
- * True when THIS Cloud Run service grants run.invoker to allUsers/allAuthenticatedUsers (public).
99
- * Reads the service's OWN IAM policy via the Run Admin API (needs run.services.getIamPolicy on the
100
- * runtime SA). On any failure we cannot tell → false (no false alarm).
101
- */
102
- async function isServicePublic() {
103
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed self-IAM read just means "posture unknown" → no warning
104
- try {
105
- const [project, region] = await Promise.all([(0, urls_1.getProjectId)(), (0, urls_1.getRegion)()]);
106
- const service = process.env['K_SERVICE'] ?? '';
107
- const url = `https://run.googleapis.com/v2/projects/${project}/locations/${region}/services/${service}:getIamPolicy`;
108
- const client = await reusableAuth.getClient();
109
- const res = await client.request({ url: url });
110
- return (res.data.bindings ?? []).some((binding) => binding.role === 'roles/run.invoker' &&
111
- (binding.members ?? []).some((m) => m === 'allUsers' || m === 'allAuthenticatedUsers'));
112
- }
113
- catch (err) {
114
- const error = (0, core_util_2.toError)(err);
115
- log.debug(`Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`);
116
- return false;
117
- }
118
- }
119
- function makeDevToken(email, audience) {
120
- const payload = JSON.stringify({ email: email, aud: audience });
121
- return DEV_TOKEN_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');
122
- }
123
- async function extractVerifiedEmail(idToken) {
124
- if (idToken.startsWith(DEV_TOKEN_PREFIX)) {
125
- return decodeDevTokenEmail(idToken);
126
- }
127
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a genuinely unverifiable token is unauthenticated (→ undefined → 401); infra failures are re-thrown below
128
- try {
129
- const ticket = await reusableVerifier.verifyIdToken({ idToken: idToken });
130
- const payload = ticket.getPayload();
131
- return payload?.email ?? undefined;
88
+ /**
89
+ * @AuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge
90
+ * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only
91
+ * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.
92
+ */
93
+ async warnIfPublicOnce() {
94
+ if (this.publicPostureChecked) {
95
+ return;
96
+ }
97
+ this.publicPostureChecked = true; // attempt at most once, even if the check itself errors
98
+ if (!(await (0, metadata_1.isOnGcp)())) {
99
+ return; // off-GCP: there is no Cloud Run edge; public/private does not apply.
100
+ }
101
+ if (await this.isServicePublic()) {
102
+ log.error('This endpoint is currently public but marked @AuthOidc which requires the service to be ' +
103
+ 'private. You are currently running in a very insecure mode; however, we are only allowing ' +
104
+ 'google-signed callers in. The edge will validate callers are allowed in (make the Cloud Run ' +
105
+ 'service private remove the allUsers run.invoker binding) to reduce your attack surface.');
106
+ }
132
107
  }
133
- catch (err) {
134
- const error = (0, core_util_2.toError)(err);
135
- if (isInfrastructureError(error)) {
136
- // A network/system failure fetching Google's certs (or a bug) is NOT an auth failure.
137
- // Do NOT mask it as a 401 — re-throw so it surfaces as a 5xx (alertable / retryable)
138
- // instead of looking like a caller sent a bad token.
139
- throw error;
140
- }
141
- // Bad signature / expired / malformed token → genuinely unauthenticated → 401 upstream.
142
- log.debug(`OIDC token rejected (unauthenticated): ${error.message}`);
143
- return undefined;
108
+ /**
109
+ * True when THIS Cloud Run service grants run.invoker to allUsers/allAuthenticatedUsers (public).
110
+ * Reads the service's OWN IAM policy via the Run Admin API (needs run.services.getIamPolicy on the
111
+ * runtime SA). On any failure we cannot tell false (no false alarm).
112
+ */
113
+ async isServicePublic() {
114
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed self-IAM read just means "posture unknown" → no warning
115
+ try {
116
+ const [project, region] = await Promise.all([(0, urls_1.getProjectId)(), (0, urls_1.getRegion)()]);
117
+ const service = process.env['K_SERVICE'] ?? '';
118
+ const url = `https://run.googleapis.com/v2/projects/${project}/locations/${region}/services/${service}:getIamPolicy`;
119
+ const client = await this.auth.getClient();
120
+ const res = await client.request({ url: url });
121
+ return (res.data.bindings ?? []).some((binding) => binding.role === 'roles/run.invoker' &&
122
+ (binding.members ?? []).some((m) => m === 'allUsers' || m === 'allAuthenticatedUsers'));
123
+ }
124
+ catch (err) {
125
+ const error = (0, core_util_2.toError)(err);
126
+ log.debug(`Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`);
127
+ return false;
128
+ }
144
129
  }
145
- }
146
- /**
147
- * True when an error thrown while verifying a token is INFRASTRUCTURE (network/system), not a bad
148
- * token — e.g. the metadata/cert fetch failed. These must NOT be swallowed as a 401 (that would
149
- * both mislead the caller and hide real outages/bugs behind a client-error status).
150
- */
151
- function isInfrastructureError(error) {
152
- const code = error.code;
153
- const networkCodes = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EAI_AGAIN'];
154
- return ((code !== undefined && networkCodes.includes(code)) ||
155
- error.name === 'GaxiosError' ||
156
- error.name === 'FetchError');
157
- }
158
- function decodeDevTokenEmail(idToken) {
159
- const encoded = idToken.substring(DEV_TOKEN_PREFIX.length);
160
- const json = Buffer.from(encoded, 'base64url').toString('utf8');
161
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed dev token is just unauthenticated → undefined
162
- try {
163
- const parsed = JSON.parse(json);
164
- return parsed.email ?? undefined;
130
+ makeDevToken(email, audience) {
131
+ const payload = JSON.stringify({ email: email, aud: audience });
132
+ return DEV_TOKEN_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');
165
133
  }
166
- catch (err) {
167
- const error = (0, core_util_2.toError)(err);
168
- log.debug(`dev-oidc token decode failed: ${error.message}`);
169
- return undefined;
134
+ async extractVerifiedEmail(idToken) {
135
+ if (idToken.startsWith(DEV_TOKEN_PREFIX)) {
136
+ return this.decodeDevTokenEmail(idToken);
137
+ }
138
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a genuinely unverifiable token is unauthenticated (→ undefined → 401); infra failures are re-thrown below
139
+ try {
140
+ const ticket = await this.verifier.verifyIdToken({ idToken: idToken });
141
+ const payload = ticket.getPayload();
142
+ return payload?.email ?? undefined;
143
+ }
144
+ catch (err) {
145
+ const error = (0, core_util_2.toError)(err);
146
+ if (this.isInfrastructureError(error)) {
147
+ // A network/system failure fetching Google's certs (or a bug) is NOT an auth failure.
148
+ // Do NOT mask it as a 401 — re-throw so it surfaces as a 5xx (alertable / retryable)
149
+ // instead of looking like a caller sent a bad token.
150
+ throw error;
151
+ }
152
+ // Bad signature / expired / malformed token → genuinely unauthenticated → 401 upstream.
153
+ log.debug(`OIDC token rejected (unauthenticated): ${error.message}`);
154
+ return undefined;
155
+ }
170
156
  }
171
- }
172
- async function resolveCallers(callers) {
173
- return Promise.all(callers.map((caller) => resolveCaller(caller)));
174
- }
175
- async function resolveCaller(caller) {
176
- if (caller === 'self') {
177
- return (0, urls_1.getRuntimeServiceAccountEmail)();
157
+ /**
158
+ * True when an error thrown while verifying a token is INFRASTRUCTURE (network/system), not a bad
159
+ * token — e.g. the metadata/cert fetch failed. These must NOT be swallowed as a 401 (that would
160
+ * both mislead the caller and hide real outages/bugs behind a client-error status).
161
+ */
162
+ isInfrastructureError(error) {
163
+ const code = error.code;
164
+ const networkCodes = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EAI_AGAIN'];
165
+ return ((code !== undefined && networkCodes.includes(code)) ||
166
+ error.name === 'GaxiosError' ||
167
+ error.name === 'FetchError');
168
+ }
169
+ decodeDevTokenEmail(idToken) {
170
+ const encoded = idToken.substring(DEV_TOKEN_PREFIX.length);
171
+ const json = Buffer.from(encoded, 'base64url').toString('utf8');
172
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed dev token is just unauthenticated → undefined
173
+ try {
174
+ const parsed = JSON.parse(json);
175
+ return parsed.email ?? undefined;
176
+ }
177
+ catch (err) {
178
+ const error = (0, core_util_2.toError)(err);
179
+ log.debug(`dev-oidc token decode failed: ${error.message}`);
180
+ return undefined;
181
+ }
178
182
  }
179
- if (caller.includes('@')) {
180
- return caller;
183
+ async resolveCallers(callers) {
184
+ return Promise.all(callers.map((caller) => this.resolveCaller(caller)));
181
185
  }
182
- const projectId = await (0, urls_1.getProjectId)();
183
- return `${caller}@${projectId}.iam.gserviceaccount.com`;
184
- }
186
+ async resolveCaller(caller) {
187
+ if (caller === 'self') {
188
+ return (0, urls_1.getRuntimeServiceAccountEmail)();
189
+ }
190
+ if (caller.includes('@')) {
191
+ return caller;
192
+ }
193
+ const projectId = await (0, urls_1.getProjectId)();
194
+ return `${caller}@${projectId}.iam.gserviceaccount.com`;
195
+ }
196
+ };
197
+ exports.GcpOidc = GcpOidc;
198
+ exports.GcpOidc = GcpOidc = tslib_1.__decorate([
199
+ (0, core_context_1.provideFrameworkSingleton)(),
200
+ (0, inversify_1.injectable)()
201
+ ], GcpOidc);
185
202
  /** Shape of the decoded dev-oidc token payload. */
186
203
  class DevTokenPayload {
187
204
  email;
package/src/oidc.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"oidc.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/oidc.ts"],"names":[],"mappings":"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","sourcesContent":["import { GoogleAuth, OAuth2Client } from 'google-auth-library';\nimport { LogManager } from '@webpieces/core-util';\nimport { toError } from '@webpieces/core-util';\nimport { isOnGcp } from './metadata';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from './urls';\n\nconst log = LogManager.getLogger('gcp-identity-oidc');\n\n/**\n * Prefix marking a deterministic local (off-GCP) OIDC token. Real Google-signed\n * tokens never start with this, so verify can tell them apart. This is what keeps\n * the @AuthOidc code path fully exercised in tests without any GCP round-trip.\n */\nconst DEV_TOKEN_PREFIX = 'dev-oidc.';\n\nconst reusableAuth = new GoogleAuth();\nconst reusableVerifier = new OAuth2Client();\n\n/** Outcome of verifying an inbound OIDC token against an allow-list of callers. */\nexport class OidcVerifyResult {\n /** True when the token is valid AND its caller SA is allowed. */\n ok: boolean;\n /** The verified caller email (present when ok). */\n email?: string;\n /** Human-readable reason when not ok (for logs / 401 message). */\n reason?: string;\n\n constructor(ok: boolean, email?: string, reason?: string) {\n this.ok = ok;\n this.email = email;\n this.reason = reason;\n }\n}\n\n/**\n * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as\n * this service's runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token\n * that verifyOidcFromCallers accepts locally.\n */\nexport async function mintIdToken(audience: string): Promise<string> {\n if (!(await isOnGcp())) {\n const email = await getRuntimeServiceAccountEmail();\n return makeDevToken(email, audience);\n }\n const client = await reusableAuth.getIdTokenClient(audience);\n return client.idTokenProvider.fetchIdToken(audience);\n}\n\n/**\n * Verify an inbound OIDC token and require its caller SA to be in the allow-list.\n * Audience is deliberately NOT gated (mirrors the platform's cross-service model —\n * the callee's own `aud` passes). Never throws — an invalid token or a disallowed\n * caller comes back as `ok:false` so callers map it to a 401 without a try/catch.\n *\n * `callers` entries resolve as: 'self' → this service's runtime SA; a bare id →\n * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' → verbatim.\n */\nexport async function verifyOidcFromCallers(\n idToken: string,\n callers: string[],\n): Promise<OidcVerifyResult> {\n const email = await extractVerifiedEmail(idToken);\n if (!email) {\n return new OidcVerifyResult(false, undefined, 'token invalid or missing email claim');\n }\n // EMPTY allow-list = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a\n // PRIVATE Cloud Run service's edge already gated WHO via run.invoker IAM (managed in terraform,\n // one source of truth). Warn loudly (once) if the service is actually PUBLIC — then the edge is\n // NOT filtering callers and this is insecure. Non-empty list = explicit app-level allow-list.\n if (callers.length === 0) {\n await warnIfPublicOnce();\n return new OidcVerifyResult(true, email);\n }\n const allowed = await resolveCallers(callers);\n if (!allowed.includes(email)) {\n return new OidcVerifyResult(\n false,\n email,\n `caller '${email}' is not in the allow-list [${allowed.join(', ')}]`,\n );\n }\n return new OidcVerifyResult(true, email);\n}\n\nlet publicPostureChecked = false;\n\n/**\n * @AuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge\n * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only\n * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.\n */\nasync function warnIfPublicOnce(): Promise<void> {\n if (publicPostureChecked) {\n return;\n }\n publicPostureChecked = true; // attempt at most once, even if the check itself errors\n if (!(await isOnGcp())) {\n return; // off-GCP: there is no Cloud Run edge; public/private does not apply.\n }\n if (await isServicePublic()) {\n log.error(\n 'This endpoint is currently public but marked @AuthOidc which requires the service to be ' +\n 'private. You are currently running in a very insecure mode; however, we are only allowing ' +\n 'google-signed callers in. The edge will validate callers are allowed in (make the Cloud Run ' +\n 'service private — remove the allUsers run.invoker binding) to reduce your attack surface.',\n );\n }\n}\n\n/**\n * True when THIS Cloud Run service grants run.invoker to allUsers/allAuthenticatedUsers (public).\n * Reads the service's OWN IAM policy via the Run Admin API (needs run.services.getIamPolicy on the\n * runtime SA). On any failure we cannot tell → false (no false alarm).\n */\nasync function isServicePublic(): Promise<boolean> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed self-IAM read just means \"posture unknown\" → no warning\n try {\n const [project, region] = await Promise.all([getProjectId(), getRegion()]);\n const service = process.env['K_SERVICE'] ?? '';\n const url = `https://run.googleapis.com/v2/projects/${project}/locations/${region}/services/${service}:getIamPolicy`;\n const client = await reusableAuth.getClient();\n const res = await client.request<IamPolicy>({ url: url });\n return (res.data.bindings ?? []).some(\n (binding: IamBinding) =>\n binding.role === 'roles/run.invoker' &&\n (binding.members ?? []).some((m: string) => m === 'allUsers' || m === 'allAuthenticatedUsers'),\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`);\n return false;\n }\n}\n\n/** Minimal shape of a Cloud Run getIamPolicy response. */\ninterface IamBinding {\n role: string;\n members?: string[];\n}\ninterface IamPolicy {\n bindings?: IamBinding[];\n}\n\nfunction makeDevToken(email: string, audience: string): string {\n const payload = JSON.stringify({ email: email, aud: audience });\n return DEV_TOKEN_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');\n}\n\nasync function extractVerifiedEmail(idToken: string): Promise<string | undefined> {\n if (idToken.startsWith(DEV_TOKEN_PREFIX)) {\n return decodeDevTokenEmail(idToken);\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a genuinely unverifiable token is unauthenticated (→ undefined → 401); infra failures are re-thrown below\n try {\n const ticket = await reusableVerifier.verifyIdToken({ idToken: idToken });\n const payload = ticket.getPayload();\n return payload?.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n if (isInfrastructureError(error)) {\n // A network/system failure fetching Google's certs (or a bug) is NOT an auth failure.\n // Do NOT mask it as a 401 — re-throw so it surfaces as a 5xx (alertable / retryable)\n // instead of looking like a caller sent a bad token.\n throw error;\n }\n // Bad signature / expired / malformed token → genuinely unauthenticated → 401 upstream.\n log.debug(`OIDC token rejected (unauthenticated): ${error.message}`);\n return undefined;\n }\n}\n\n/**\n * True when an error thrown while verifying a token is INFRASTRUCTURE (network/system), not a bad\n * token — e.g. the metadata/cert fetch failed. These must NOT be swallowed as a 401 (that would\n * both mislead the caller and hide real outages/bugs behind a client-error status).\n */\nfunction isInfrastructureError(error: Error): boolean {\n const code = (error as ErrorWithCode).code;\n const networkCodes = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EAI_AGAIN'];\n return (\n (code !== undefined && networkCodes.includes(code)) ||\n error.name === 'GaxiosError' ||\n error.name === 'FetchError'\n );\n}\n\nfunction decodeDevTokenEmail(idToken: string): string | undefined {\n const encoded = idToken.substring(DEV_TOKEN_PREFIX.length);\n const json = Buffer.from(encoded, 'base64url').toString('utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed dev token is just unauthenticated → undefined\n try {\n const parsed = JSON.parse(json) as DevTokenPayload;\n return parsed.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`dev-oidc token decode failed: ${error.message}`);\n return undefined;\n }\n}\n\nasync function resolveCallers(callers: string[]): Promise<string[]> {\n return Promise.all(callers.map((caller: string) => resolveCaller(caller)));\n}\n\nasync function resolveCaller(caller: string): Promise<string> {\n if (caller === 'self') {\n return getRuntimeServiceAccountEmail();\n }\n if (caller.includes('@')) {\n return caller;\n }\n const projectId = await getProjectId();\n return `${caller}@${projectId}.iam.gserviceaccount.com`;\n}\n\n/** Shape of the decoded dev-oidc token payload. */\nclass DevTokenPayload {\n email!: string;\n aud!: string;\n}\n\n/** A thrown error carrying an optional Node/system error code (e.g. 'ETIMEDOUT'). */\ninterface ErrorWithCode {\n code?: string;\n}\n"]}
1
+ {"version":3,"file":"oidc.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/oidc.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable } from 'inversify';\nimport { GoogleAuth, OAuth2Client } from 'google-auth-library';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { toError } from '@webpieces/core-util';\nimport { isOnGcp } from './metadata';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from './urls';\n\nconst log = LogManager.getLogger('gcp-identity-oidc');\n\n/**\n * Prefix marking a deterministic local (off-GCP) OIDC token. Real Google-signed\n * tokens never start with this, so verify can tell them apart. This is what keeps\n * the @AuthOidc code path fully exercised in tests without any GCP round-trip.\n */\nconst DEV_TOKEN_PREFIX = 'dev-oidc.';\n\n/** Outcome of verifying an inbound OIDC token against an allow-list of callers. */\nexport class OidcVerifyResult {\n /** True when the token is valid AND its caller SA is allowed. */\n ok: boolean;\n /** The verified caller email (present when ok). */\n email?: string;\n /** Human-readable reason when not ok (for logs / 401 message). */\n reason?: string;\n\n constructor(ok: boolean, email?: string, reason?: string) {\n this.ok = ok;\n this.email = email;\n this.reason = reason;\n }\n}\n\n/**\n * GcpOidc - Google OIDC service-to-service tokens: MINT an outbound token as this service's runtime\n * SA, and VERIFY an inbound token against a caller allow-list. Injected by type (@provideSingleton /\n * inject-by-type) into the RPC + Cloud Tasks clients (mint) and the framework auth verifier (verify),\n * so no DI-managed code calls gcp-identity as a free function.\n *\n * Holds the reusable google-auth-library clients as instance state (one per process, since this is a\n * framework singleton) plus the \"warned about public posture once\" flag.\n */\n@provideFrameworkSingleton()\n@injectable()\nexport class GcpOidc {\n private readonly auth = new GoogleAuth();\n private readonly verifier = new OAuth2Client();\n private publicPostureChecked = false;\n\n /**\n * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as this service's\n * runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token that {@link verifyFromCallers}\n * accepts locally.\n */\n async mintIdToken(audience: string): Promise<string> {\n if (!(await isOnGcp())) {\n const email = await getRuntimeServiceAccountEmail();\n return this.makeDevToken(email, audience);\n }\n const client = await this.auth.getIdTokenClient(audience);\n return client.idTokenProvider.fetchIdToken(audience);\n }\n\n /**\n * Verify an inbound OIDC token and require its caller SA to be in the allow-list. Audience is\n * deliberately NOT gated (mirrors the platform's cross-service model — the callee's own `aud`\n * passes). Never throws — an invalid token or a disallowed caller comes back as `ok:false` so\n * callers map it to a 401 without a try/catch.\n *\n * `callers` entries resolve as: 'self' → this service's runtime SA; a bare id →\n * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' → verbatim.\n */\n async verifyFromCallers(idToken: string, callers: string[]): Promise<OidcVerifyResult> {\n const email = await this.extractVerifiedEmail(idToken);\n if (!email) {\n return new OidcVerifyResult(false, undefined, 'token invalid or missing email claim');\n }\n // EMPTY allow-list = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a\n // PRIVATE Cloud Run service's edge already gated WHO via run.invoker IAM (managed in terraform,\n // one source of truth). Warn loudly (once) if the service is actually PUBLIC — then the edge is\n // NOT filtering callers and this is insecure. Non-empty list = explicit app-level allow-list.\n if (callers.length === 0) {\n await this.warnIfPublicOnce();\n return new OidcVerifyResult(true, email);\n }\n const allowed = await this.resolveCallers(callers);\n if (!allowed.includes(email)) {\n return new OidcVerifyResult(\n false,\n email,\n `caller '${email}' is not in the allow-list [${allowed.join(', ')}]`,\n );\n }\n return new OidcVerifyResult(true, email);\n }\n\n /**\n * @AuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge\n * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only\n * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.\n */\n private async warnIfPublicOnce(): Promise<void> {\n if (this.publicPostureChecked) {\n return;\n }\n this.publicPostureChecked = true; // attempt at most once, even if the check itself errors\n if (!(await isOnGcp())) {\n return; // off-GCP: there is no Cloud Run edge; public/private does not apply.\n }\n if (await this.isServicePublic()) {\n log.error(\n 'This endpoint is currently public but marked @AuthOidc which requires the service to be ' +\n 'private. You are currently running in a very insecure mode; however, we are only allowing ' +\n 'google-signed callers in. The edge will validate callers are allowed in (make the Cloud Run ' +\n 'service private — remove the allUsers run.invoker binding) to reduce your attack surface.',\n );\n }\n }\n\n /**\n * True when THIS Cloud Run service grants run.invoker to allUsers/allAuthenticatedUsers (public).\n * Reads the service's OWN IAM policy via the Run Admin API (needs run.services.getIamPolicy on the\n * runtime SA). On any failure we cannot tell → false (no false alarm).\n */\n private async isServicePublic(): Promise<boolean> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed self-IAM read just means \"posture unknown\" → no warning\n try {\n const [project, region] = await Promise.all([getProjectId(), getRegion()]);\n const service = process.env['K_SERVICE'] ?? '';\n const url = `https://run.googleapis.com/v2/projects/${project}/locations/${region}/services/${service}:getIamPolicy`;\n const client = await this.auth.getClient();\n const res = await client.request<IamPolicy>({ url: url });\n return (res.data.bindings ?? []).some(\n (binding: IamBinding) =>\n binding.role === 'roles/run.invoker' &&\n (binding.members ?? []).some((m: string) => m === 'allUsers' || m === 'allAuthenticatedUsers'),\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`);\n return false;\n }\n }\n\n private makeDevToken(email: string, audience: string): string {\n const payload = JSON.stringify({ email: email, aud: audience });\n return DEV_TOKEN_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');\n }\n\n private async extractVerifiedEmail(idToken: string): Promise<string | undefined> {\n if (idToken.startsWith(DEV_TOKEN_PREFIX)) {\n return this.decodeDevTokenEmail(idToken);\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a genuinely unverifiable token is unauthenticated (→ undefined → 401); infra failures are re-thrown below\n try {\n const ticket = await this.verifier.verifyIdToken({ idToken: idToken });\n const payload = ticket.getPayload();\n return payload?.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n if (this.isInfrastructureError(error)) {\n // A network/system failure fetching Google's certs (or a bug) is NOT an auth failure.\n // Do NOT mask it as a 401 — re-throw so it surfaces as a 5xx (alertable / retryable)\n // instead of looking like a caller sent a bad token.\n throw error;\n }\n // Bad signature / expired / malformed token → genuinely unauthenticated → 401 upstream.\n log.debug(`OIDC token rejected (unauthenticated): ${error.message}`);\n return undefined;\n }\n }\n\n /**\n * True when an error thrown while verifying a token is INFRASTRUCTURE (network/system), not a bad\n * token — e.g. the metadata/cert fetch failed. These must NOT be swallowed as a 401 (that would\n * both mislead the caller and hide real outages/bugs behind a client-error status).\n */\n private isInfrastructureError(error: Error): boolean {\n const code = (error as ErrorWithCode).code;\n const networkCodes = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EAI_AGAIN'];\n return (\n (code !== undefined && networkCodes.includes(code)) ||\n error.name === 'GaxiosError' ||\n error.name === 'FetchError'\n );\n }\n\n private decodeDevTokenEmail(idToken: string): string | undefined {\n const encoded = idToken.substring(DEV_TOKEN_PREFIX.length);\n const json = Buffer.from(encoded, 'base64url').toString('utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed dev token is just unauthenticated → undefined\n try {\n const parsed = JSON.parse(json) as DevTokenPayload;\n return parsed.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`dev-oidc token decode failed: ${error.message}`);\n return undefined;\n }\n }\n\n private async resolveCallers(callers: string[]): Promise<string[]> {\n return Promise.all(callers.map((caller: string) => this.resolveCaller(caller)));\n }\n\n private async resolveCaller(caller: string): Promise<string> {\n if (caller === 'self') {\n return getRuntimeServiceAccountEmail();\n }\n if (caller.includes('@')) {\n return caller;\n }\n const projectId = await getProjectId();\n return `${caller}@${projectId}.iam.gserviceaccount.com`;\n }\n}\n\n/** Minimal shape of a Cloud Run getIamPolicy response. */\ninterface IamBinding {\n role: string;\n members?: string[];\n}\ninterface IamPolicy {\n bindings?: IamBinding[];\n}\n\n/** Shape of the decoded dev-oidc token payload. */\nclass DevTokenPayload {\n email!: string;\n aud!: string;\n}\n\n/** A thrown error carrying an optional Node/system error code (e.g. 'ETIMEDOUT'). */\ninterface ErrorWithCode {\n code?: string;\n}\n"]}