@el-j/google-sheet-translations 2.2.0-beta.4 → 2.2.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -378,6 +378,23 @@ jobs:
378
378
  | `spreadsheet-title` | ❌ | `google-sheet-translations` | Title for the auto-created spreadsheet |
379
379
  | `source-locale` | ❌ | `en` | Source locale used as the base for auto-translate formulas |
380
380
  | `target-locales` | ❌ | `de,fr,es,it,pt,ja,zh` | Comma-separated target locales added to the auto-created spreadsheet |
381
+ | `drive-folder-id` | ❌ | `''` | Drive folder ID used to auto-discover spreadsheets, docs, and images |
382
+ | `scan-for-spreadsheets` | ❌ | `true` | When `drive-folder-id` is set, scan the folder recursively for spreadsheets |
383
+ | `spreadsheet-ids` | ❌ | `''` | Optional extra spreadsheet IDs to merge with any IDs discovered in Drive |
384
+ | `sync-images` | ❌ | `false` | Download Drive images alongside translation sync |
385
+ | `image-output-path` | ❌ | `./public/remote-images` | Output directory for downloaded Drive images when `sync-images` is enabled |
386
+
387
+ ### Drive Folder Mode
388
+
389
+ When `drive-folder-id` is set, the action switches from single-spreadsheet mode to `manageDriveTranslations()`. That enables recursive spreadsheet discovery, optional image sync, and optional explicit `spreadsheet-ids` on the same run.
390
+
391
+ Operational notes:
392
+
393
+ - If the Drive folder is empty and `auto-create` remains enabled, the package creates a spreadsheet and then tries to move it into the target folder.
394
+ - `spreadsheetNameFilter` only applies to discovered sheets in the programmatic API. Explicit `spreadsheet-ids` are never filtered out.
395
+ - `sync-images` supports incremental freshness checks and optional `cleanSync` deletion in the programmatic API. See the Drive Folder guide for those semantics before enabling destructive cleanup.
396
+
397
+ See the full Drive Folder guide in `website/guide/drive-folder.md` for the complete API surface, image sync semantics, and Google Doc ingestion details.
381
398
 
382
399
  ### Outputs
383
400
 
package/dist/esm/index.js CHANGED
@@ -2070,6 +2070,9 @@ function entriesToSeedKeys(entries) {
2070
2070
  return keys;
2071
2071
  }
2072
2072
  function entriesToTranslationData(entries, locale) {
2073
+ if (locale === "__proto__" || locale === "constructor" || locale === "prototype") {
2074
+ return {};
2075
+ }
2073
2076
  const data = {};
2074
2077
  data[locale] = {};
2075
2078
  const counts = /* @__PURE__ */ new Map();
@@ -2398,11 +2401,244 @@ async function manageDriveTranslations(options) {
2398
2401
  return { translations, spreadsheetIds: filteredIds, imageSync, manifest, docIngestResults };
2399
2402
  }
2400
2403
 
2404
+ // src/setup/wifSetup.ts
2405
+ import { GoogleAuth as GoogleAuth4 } from "google-auth-library";
2406
+ var GcpApiError = class extends Error {
2407
+ constructor(message, status) {
2408
+ super(message);
2409
+ this.status = status;
2410
+ this.name = "GcpApiError";
2411
+ }
2412
+ };
2413
+ async function getAccessToken4(keyFilePath) {
2414
+ const auth = new GoogleAuth4({
2415
+ ...keyFilePath ? { keyFilename: keyFilePath } : {},
2416
+ scopes: ["https://www.googleapis.com/auth/cloud-platform"]
2417
+ });
2418
+ const client = await auth.getClient();
2419
+ const tokenResponse = await client.getAccessToken();
2420
+ if (!tokenResponse.token) {
2421
+ throw new Error(
2422
+ "Failed to obtain a Google Cloud access token. Ensure you are authenticated via Application Default Credentials (run: gcloud auth application-default login) or provide --key-file pointing to a service account JSON key."
2423
+ );
2424
+ }
2425
+ return tokenResponse.token;
2426
+ }
2427
+ async function gcpFetch(url, token, method = "GET", body) {
2428
+ const response = await fetch(url, {
2429
+ method,
2430
+ headers: {
2431
+ Authorization: `Bearer ${token}`,
2432
+ "Content-Type": "application/json"
2433
+ },
2434
+ body: body !== void 0 ? JSON.stringify(body) : void 0
2435
+ });
2436
+ const data = await response.json();
2437
+ if (!response.ok) {
2438
+ const errData = data;
2439
+ const message = errData.error?.message ?? `HTTP ${response.status}`;
2440
+ throw new GcpApiError(message, response.status);
2441
+ }
2442
+ return data;
2443
+ }
2444
+ async function pollOperation(operationName, token, maxWaitMs = 6e4) {
2445
+ let opUrl;
2446
+ if (operationName.startsWith("http")) {
2447
+ let isGoogleHost;
2448
+ try {
2449
+ const parsedUrl = new URL(operationName);
2450
+ const hostname = (parsedUrl.hostname || "").toLowerCase();
2451
+ isGoogleHost = hostname === "iam.googleapis.com" || hostname.endsWith(".iam.googleapis.com") || hostname === "googleapis.com" || hostname.endsWith(".googleapis.com");
2452
+ } catch {
2453
+ isGoogleHost = false;
2454
+ }
2455
+ if (!isGoogleHost) {
2456
+ throw new Error(
2457
+ `Invalid operation URL: hostname must be a Google API endpoint (*.googleapis.com), got: ${operationName}`
2458
+ );
2459
+ }
2460
+ opUrl = operationName;
2461
+ } else {
2462
+ opUrl = `https://iam.googleapis.com/v1/${operationName}`;
2463
+ }
2464
+ const deadline = Date.now() + maxWaitMs;
2465
+ const maxWaitSecs = Math.round(maxWaitMs / 1e3);
2466
+ while (Date.now() < deadline) {
2467
+ const op = await gcpFetch(opUrl, token);
2468
+ if (op.done) {
2469
+ if (op.error) {
2470
+ throw new Error(`Operation failed: ${op.error.message}`);
2471
+ }
2472
+ return;
2473
+ }
2474
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2475
+ }
2476
+ if (Date.now() >= deadline) {
2477
+ throw new Error(
2478
+ `Operation timed out after ${maxWaitSecs} s. The resources may still be provisioning in the background \u2013 re-running the command is safe (existing resources are reused).`
2479
+ );
2480
+ }
2481
+ }
2482
+ async function getProjectNumber(projectId, token) {
2483
+ const data = await gcpFetch(
2484
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(projectId)}`,
2485
+ token
2486
+ );
2487
+ return data.projectNumber;
2488
+ }
2489
+ async function createOrGetWifPool(projectId, poolId, token) {
2490
+ try {
2491
+ const op = await gcpFetch(
2492
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools?workloadIdentityPoolId=${encodeURIComponent(poolId)}`,
2493
+ token,
2494
+ "POST",
2495
+ {
2496
+ displayName: "GitHub Actions Pool",
2497
+ description: "Pool for GitHub Actions OIDC authentication",
2498
+ disabled: false
2499
+ }
2500
+ );
2501
+ if (!op.done) {
2502
+ await pollOperation(op.name, token);
2503
+ } else if (op.error) {
2504
+ throw new Error(`Operation failed: ${op.error.message}`);
2505
+ }
2506
+ } catch (err) {
2507
+ if (err instanceof GcpApiError && err.status === 409) {
2508
+ return;
2509
+ }
2510
+ throw err;
2511
+ }
2512
+ }
2513
+ async function createOrGetWifProvider(projectId, poolId, providerId, githubRepo, token) {
2514
+ try {
2515
+ const op = await gcpFetch(
2516
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools/${encodeURIComponent(poolId)}/providers?workloadIdentityPoolProviderId=${encodeURIComponent(providerId)}`,
2517
+ token,
2518
+ "POST",
2519
+ {
2520
+ displayName: "GitHub OIDC Provider",
2521
+ disabled: false,
2522
+ attributeMapping: {
2523
+ "google.subject": "assertion.sub",
2524
+ "attribute.actor": "assertion.actor",
2525
+ "attribute.repository": "assertion.repository"
2526
+ },
2527
+ // Scope the provider to this exact repository for security
2528
+ attributeCondition: `assertion.repository=='${githubRepo}'`,
2529
+ oidc: {
2530
+ issuerUri: "https://token.actions.githubusercontent.com"
2531
+ }
2532
+ }
2533
+ );
2534
+ if (!op.done) {
2535
+ await pollOperation(op.name, token);
2536
+ } else if (op.error) {
2537
+ throw new Error(`Operation failed: ${op.error.message}`);
2538
+ }
2539
+ } catch (err) {
2540
+ if (err instanceof GcpApiError && err.status === 409) {
2541
+ return;
2542
+ }
2543
+ throw err;
2544
+ }
2545
+ }
2546
+ async function bindServiceAccount(projectId, serviceAccountEmail, projectNumber, poolId, githubRepo, token) {
2547
+ const saResource = `projects/${encodeURIComponent(projectId)}/serviceAccounts/${encodeURIComponent(serviceAccountEmail)}`;
2548
+ const principal = `principalSet://iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/attribute.repository/${githubRepo}`;
2549
+ const policy = await gcpFetch(
2550
+ `https://iam.googleapis.com/v1/${saResource}:getIamPolicy`,
2551
+ token,
2552
+ "POST",
2553
+ {}
2554
+ );
2555
+ const bindings = policy.bindings ?? [];
2556
+ const role = "roles/iam.workloadIdentityUser";
2557
+ const existing = bindings.find((b) => b.role === role);
2558
+ if (existing) {
2559
+ if (existing.members.includes(principal)) {
2560
+ return;
2561
+ }
2562
+ existing.members.push(principal);
2563
+ } else {
2564
+ bindings.push({ role, members: [principal] });
2565
+ }
2566
+ await gcpFetch(
2567
+ `https://iam.googleapis.com/v1/${saResource}:setIamPolicy`,
2568
+ token,
2569
+ "POST",
2570
+ { policy: { ...policy, bindings } }
2571
+ );
2572
+ }
2573
+ async function setupWIF(options) {
2574
+ const poolId = options.poolId ?? "github-actions";
2575
+ const providerId = options.providerId ?? "github-oidc";
2576
+ const log = options.onProgress ?? (() => void 0);
2577
+ if (!options.githubRepo.includes("/")) {
2578
+ throw new Error(
2579
+ `githubRepo must be in "owner/repo" format, got: "${options.githubRepo}"`
2580
+ );
2581
+ }
2582
+ log("Authenticating with Google Cloud...");
2583
+ const token = await getAccessToken4(options.keyFilePath);
2584
+ log("Fetching project number...");
2585
+ const projectNumber = await getProjectNumber(options.projectId, token);
2586
+ log(`Creating Workload Identity Pool "${poolId}"...`);
2587
+ await createOrGetWifPool(options.projectId, poolId, token);
2588
+ log(`Creating OIDC Provider "${providerId}"...`);
2589
+ await createOrGetWifProvider(
2590
+ options.projectId,
2591
+ poolId,
2592
+ providerId,
2593
+ options.githubRepo,
2594
+ token
2595
+ );
2596
+ log("Binding service account permissions...");
2597
+ await bindServiceAccount(
2598
+ options.projectId,
2599
+ options.serviceAccountEmail,
2600
+ projectNumber,
2601
+ poolId,
2602
+ options.githubRepo,
2603
+ token
2604
+ );
2605
+ const wifProvider = `projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/providers/${providerId}`;
2606
+ return { wifProvider, projectNumber, poolId, providerId };
2607
+ }
2608
+ async function grantDrivePermissions(options) {
2609
+ const token = await getAccessToken4(options.keyFilePath);
2610
+ const policy = await gcpFetch(
2611
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:getIamPolicy`,
2612
+ token,
2613
+ "POST",
2614
+ {}
2615
+ );
2616
+ const bindings = policy.bindings ?? [];
2617
+ const role = "roles/drive.file";
2618
+ const member = `serviceAccount:${options.serviceAccountEmail}`;
2619
+ const existing = bindings.find((b) => b.role === role);
2620
+ if (existing) {
2621
+ if (existing.members.includes(member)) {
2622
+ return;
2623
+ }
2624
+ existing.members.push(member);
2625
+ } else {
2626
+ bindings.push({ role, members: [member] });
2627
+ }
2628
+ await gcpFetch(
2629
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:setIamPolicy`,
2630
+ token,
2631
+ "POST",
2632
+ { policy: { ...policy, bindings } }
2633
+ );
2634
+ }
2635
+
2401
2636
  // src/index.ts
2402
2637
  var index_default = getSpreadSheetData;
2403
2638
  export {
2404
2639
  DEFAULT_IMAGE_EXTENSIONS,
2405
2640
  DEFAULT_WAIT_SECONDS,
2641
+ GcpApiError,
2406
2642
  buildGoogleAuth,
2407
2643
  buildManifest,
2408
2644
  convertFromDataJsonFormat,
@@ -2424,6 +2660,7 @@ export {
2424
2660
  getOriginalHeaderForLocale,
2425
2661
  getSpreadSheetData,
2426
2662
  getTranslationSummary,
2663
+ grantDrivePermissions,
2427
2664
  handleBidirectionalSync,
2428
2665
  inferLocaleFromDocName,
2429
2666
  ingestDoc,
@@ -2441,6 +2678,7 @@ export {
2441
2678
  resolveLocaleWithFallback,
2442
2679
  scanDriveFolderForDocs,
2443
2680
  scanDriveFolderForSpreadsheets,
2681
+ setupWIF,
2444
2682
  slugifyKey,
2445
2683
  syncDriveImages,
2446
2684
  updateSpreadsheetWithLocalChanges,
package/dist/index.d.ts CHANGED
@@ -44,6 +44,8 @@ export { parseDocContent, slugifyKey } from './utils/docParser';
44
44
  export type { ParsedDocEntry, DocKeyStrategy, ParseDocOptions } from './utils/docParser';
45
45
  export { ingestDoc, exportDoc, entriesToSeedKeys, entriesToTranslationData } from './utils/docIngester';
46
46
  export type { DocIngesterOptions, DocIngestResult, DocUpdateMode } from './utils/docIngester';
47
+ export { setupWIF, grantDrivePermissions, GcpApiError } from './setup/wifSetup';
48
+ export type { WifSetupOptions, WifSetupResult, GrantDrivePermissionsOptions } from './setup/wifSetup';
47
49
  import { getSpreadSheetData } from './getSpreadSheetData';
48
50
  export default getSpreadSheetData;
49
51
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_IMAGE_EXTENSIONS: () => DEFAULT_IMAGE_EXTENSIONS,
34
34
  DEFAULT_WAIT_SECONDS: () => DEFAULT_WAIT_SECONDS,
35
+ GcpApiError: () => GcpApiError,
35
36
  buildGoogleAuth: () => buildGoogleAuth,
36
37
  buildManifest: () => buildManifest,
37
38
  convertFromDataJsonFormat: () => convertFromDataJsonFormat,
@@ -53,6 +54,7 @@ __export(index_exports, {
53
54
  getOriginalHeaderForLocale: () => getOriginalHeaderForLocale,
54
55
  getSpreadSheetData: () => getSpreadSheetData,
55
56
  getTranslationSummary: () => getTranslationSummary,
57
+ grantDrivePermissions: () => grantDrivePermissions,
56
58
  handleBidirectionalSync: () => handleBidirectionalSync,
57
59
  inferLocaleFromDocName: () => inferLocaleFromDocName,
58
60
  ingestDoc: () => ingestDoc,
@@ -70,6 +72,7 @@ __export(index_exports, {
70
72
  resolveLocaleWithFallback: () => resolveLocaleWithFallback,
71
73
  scanDriveFolderForDocs: () => scanDriveFolderForDocs,
72
74
  scanDriveFolderForSpreadsheets: () => scanDriveFolderForSpreadsheets,
75
+ setupWIF: () => setupWIF,
73
76
  slugifyKey: () => slugifyKey,
74
77
  syncDriveImages: () => syncDriveImages,
75
78
  updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
@@ -2158,6 +2161,9 @@ function entriesToSeedKeys(entries) {
2158
2161
  return keys;
2159
2162
  }
2160
2163
  function entriesToTranslationData(entries, locale) {
2164
+ if (locale === "__proto__" || locale === "constructor" || locale === "prototype") {
2165
+ return {};
2166
+ }
2161
2167
  const data = {};
2162
2168
  data[locale] = {};
2163
2169
  const counts = /* @__PURE__ */ new Map();
@@ -2486,12 +2492,245 @@ async function manageDriveTranslations(options) {
2486
2492
  return { translations, spreadsheetIds: filteredIds, imageSync, manifest, docIngestResults };
2487
2493
  }
2488
2494
 
2495
+ // src/setup/wifSetup.ts
2496
+ var import_google_auth_library4 = require("google-auth-library");
2497
+ var GcpApiError = class extends Error {
2498
+ constructor(message, status) {
2499
+ super(message);
2500
+ this.status = status;
2501
+ this.name = "GcpApiError";
2502
+ }
2503
+ };
2504
+ async function getAccessToken4(keyFilePath) {
2505
+ const auth = new import_google_auth_library4.GoogleAuth({
2506
+ ...keyFilePath ? { keyFilename: keyFilePath } : {},
2507
+ scopes: ["https://www.googleapis.com/auth/cloud-platform"]
2508
+ });
2509
+ const client = await auth.getClient();
2510
+ const tokenResponse = await client.getAccessToken();
2511
+ if (!tokenResponse.token) {
2512
+ throw new Error(
2513
+ "Failed to obtain a Google Cloud access token. Ensure you are authenticated via Application Default Credentials (run: gcloud auth application-default login) or provide --key-file pointing to a service account JSON key."
2514
+ );
2515
+ }
2516
+ return tokenResponse.token;
2517
+ }
2518
+ async function gcpFetch(url, token, method = "GET", body) {
2519
+ const response = await fetch(url, {
2520
+ method,
2521
+ headers: {
2522
+ Authorization: `Bearer ${token}`,
2523
+ "Content-Type": "application/json"
2524
+ },
2525
+ body: body !== void 0 ? JSON.stringify(body) : void 0
2526
+ });
2527
+ const data = await response.json();
2528
+ if (!response.ok) {
2529
+ const errData = data;
2530
+ const message = errData.error?.message ?? `HTTP ${response.status}`;
2531
+ throw new GcpApiError(message, response.status);
2532
+ }
2533
+ return data;
2534
+ }
2535
+ async function pollOperation(operationName, token, maxWaitMs = 6e4) {
2536
+ let opUrl;
2537
+ if (operationName.startsWith("http")) {
2538
+ let isGoogleHost;
2539
+ try {
2540
+ const parsedUrl = new URL(operationName);
2541
+ const hostname = (parsedUrl.hostname || "").toLowerCase();
2542
+ isGoogleHost = hostname === "iam.googleapis.com" || hostname.endsWith(".iam.googleapis.com") || hostname === "googleapis.com" || hostname.endsWith(".googleapis.com");
2543
+ } catch {
2544
+ isGoogleHost = false;
2545
+ }
2546
+ if (!isGoogleHost) {
2547
+ throw new Error(
2548
+ `Invalid operation URL: hostname must be a Google API endpoint (*.googleapis.com), got: ${operationName}`
2549
+ );
2550
+ }
2551
+ opUrl = operationName;
2552
+ } else {
2553
+ opUrl = `https://iam.googleapis.com/v1/${operationName}`;
2554
+ }
2555
+ const deadline = Date.now() + maxWaitMs;
2556
+ const maxWaitSecs = Math.round(maxWaitMs / 1e3);
2557
+ while (Date.now() < deadline) {
2558
+ const op = await gcpFetch(opUrl, token);
2559
+ if (op.done) {
2560
+ if (op.error) {
2561
+ throw new Error(`Operation failed: ${op.error.message}`);
2562
+ }
2563
+ return;
2564
+ }
2565
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2566
+ }
2567
+ if (Date.now() >= deadline) {
2568
+ throw new Error(
2569
+ `Operation timed out after ${maxWaitSecs} s. The resources may still be provisioning in the background \u2013 re-running the command is safe (existing resources are reused).`
2570
+ );
2571
+ }
2572
+ }
2573
+ async function getProjectNumber(projectId, token) {
2574
+ const data = await gcpFetch(
2575
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(projectId)}`,
2576
+ token
2577
+ );
2578
+ return data.projectNumber;
2579
+ }
2580
+ async function createOrGetWifPool(projectId, poolId, token) {
2581
+ try {
2582
+ const op = await gcpFetch(
2583
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools?workloadIdentityPoolId=${encodeURIComponent(poolId)}`,
2584
+ token,
2585
+ "POST",
2586
+ {
2587
+ displayName: "GitHub Actions Pool",
2588
+ description: "Pool for GitHub Actions OIDC authentication",
2589
+ disabled: false
2590
+ }
2591
+ );
2592
+ if (!op.done) {
2593
+ await pollOperation(op.name, token);
2594
+ } else if (op.error) {
2595
+ throw new Error(`Operation failed: ${op.error.message}`);
2596
+ }
2597
+ } catch (err) {
2598
+ if (err instanceof GcpApiError && err.status === 409) {
2599
+ return;
2600
+ }
2601
+ throw err;
2602
+ }
2603
+ }
2604
+ async function createOrGetWifProvider(projectId, poolId, providerId, githubRepo, token) {
2605
+ try {
2606
+ const op = await gcpFetch(
2607
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools/${encodeURIComponent(poolId)}/providers?workloadIdentityPoolProviderId=${encodeURIComponent(providerId)}`,
2608
+ token,
2609
+ "POST",
2610
+ {
2611
+ displayName: "GitHub OIDC Provider",
2612
+ disabled: false,
2613
+ attributeMapping: {
2614
+ "google.subject": "assertion.sub",
2615
+ "attribute.actor": "assertion.actor",
2616
+ "attribute.repository": "assertion.repository"
2617
+ },
2618
+ // Scope the provider to this exact repository for security
2619
+ attributeCondition: `assertion.repository=='${githubRepo}'`,
2620
+ oidc: {
2621
+ issuerUri: "https://token.actions.githubusercontent.com"
2622
+ }
2623
+ }
2624
+ );
2625
+ if (!op.done) {
2626
+ await pollOperation(op.name, token);
2627
+ } else if (op.error) {
2628
+ throw new Error(`Operation failed: ${op.error.message}`);
2629
+ }
2630
+ } catch (err) {
2631
+ if (err instanceof GcpApiError && err.status === 409) {
2632
+ return;
2633
+ }
2634
+ throw err;
2635
+ }
2636
+ }
2637
+ async function bindServiceAccount(projectId, serviceAccountEmail, projectNumber, poolId, githubRepo, token) {
2638
+ const saResource = `projects/${encodeURIComponent(projectId)}/serviceAccounts/${encodeURIComponent(serviceAccountEmail)}`;
2639
+ const principal = `principalSet://iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/attribute.repository/${githubRepo}`;
2640
+ const policy = await gcpFetch(
2641
+ `https://iam.googleapis.com/v1/${saResource}:getIamPolicy`,
2642
+ token,
2643
+ "POST",
2644
+ {}
2645
+ );
2646
+ const bindings = policy.bindings ?? [];
2647
+ const role = "roles/iam.workloadIdentityUser";
2648
+ const existing = bindings.find((b) => b.role === role);
2649
+ if (existing) {
2650
+ if (existing.members.includes(principal)) {
2651
+ return;
2652
+ }
2653
+ existing.members.push(principal);
2654
+ } else {
2655
+ bindings.push({ role, members: [principal] });
2656
+ }
2657
+ await gcpFetch(
2658
+ `https://iam.googleapis.com/v1/${saResource}:setIamPolicy`,
2659
+ token,
2660
+ "POST",
2661
+ { policy: { ...policy, bindings } }
2662
+ );
2663
+ }
2664
+ async function setupWIF(options) {
2665
+ const poolId = options.poolId ?? "github-actions";
2666
+ const providerId = options.providerId ?? "github-oidc";
2667
+ const log = options.onProgress ?? (() => void 0);
2668
+ if (!options.githubRepo.includes("/")) {
2669
+ throw new Error(
2670
+ `githubRepo must be in "owner/repo" format, got: "${options.githubRepo}"`
2671
+ );
2672
+ }
2673
+ log("Authenticating with Google Cloud...");
2674
+ const token = await getAccessToken4(options.keyFilePath);
2675
+ log("Fetching project number...");
2676
+ const projectNumber = await getProjectNumber(options.projectId, token);
2677
+ log(`Creating Workload Identity Pool "${poolId}"...`);
2678
+ await createOrGetWifPool(options.projectId, poolId, token);
2679
+ log(`Creating OIDC Provider "${providerId}"...`);
2680
+ await createOrGetWifProvider(
2681
+ options.projectId,
2682
+ poolId,
2683
+ providerId,
2684
+ options.githubRepo,
2685
+ token
2686
+ );
2687
+ log("Binding service account permissions...");
2688
+ await bindServiceAccount(
2689
+ options.projectId,
2690
+ options.serviceAccountEmail,
2691
+ projectNumber,
2692
+ poolId,
2693
+ options.githubRepo,
2694
+ token
2695
+ );
2696
+ const wifProvider = `projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/providers/${providerId}`;
2697
+ return { wifProvider, projectNumber, poolId, providerId };
2698
+ }
2699
+ async function grantDrivePermissions(options) {
2700
+ const token = await getAccessToken4(options.keyFilePath);
2701
+ const policy = await gcpFetch(
2702
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:getIamPolicy`,
2703
+ token,
2704
+ "POST",
2705
+ {}
2706
+ );
2707
+ const bindings = policy.bindings ?? [];
2708
+ const role = "roles/drive.file";
2709
+ const member = `serviceAccount:${options.serviceAccountEmail}`;
2710
+ const existing = bindings.find((b) => b.role === role);
2711
+ if (existing) {
2712
+ if (existing.members.includes(member)) {
2713
+ return;
2714
+ }
2715
+ existing.members.push(member);
2716
+ } else {
2717
+ bindings.push({ role, members: [member] });
2718
+ }
2719
+ await gcpFetch(
2720
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:setIamPolicy`,
2721
+ token,
2722
+ "POST",
2723
+ { policy: { ...policy, bindings } }
2724
+ );
2725
+ }
2726
+
2489
2727
  // src/index.ts
2490
2728
  var index_default = getSpreadSheetData;
2491
2729
  // Annotate the CommonJS export names for ESM import in node:
2492
2730
  0 && (module.exports = {
2493
2731
  DEFAULT_IMAGE_EXTENSIONS,
2494
2732
  DEFAULT_WAIT_SECONDS,
2733
+ GcpApiError,
2495
2734
  buildGoogleAuth,
2496
2735
  buildManifest,
2497
2736
  convertFromDataJsonFormat,
@@ -2512,6 +2751,7 @@ var index_default = getSpreadSheetData;
2512
2751
  getOriginalHeaderForLocale,
2513
2752
  getSpreadSheetData,
2514
2753
  getTranslationSummary,
2754
+ grantDrivePermissions,
2515
2755
  handleBidirectionalSync,
2516
2756
  inferLocaleFromDocName,
2517
2757
  ingestDoc,
@@ -2529,6 +2769,7 @@ var index_default = getSpreadSheetData;
2529
2769
  resolveLocaleWithFallback,
2530
2770
  scanDriveFolderForDocs,
2531
2771
  scanDriveFolderForSpreadsheets,
2772
+ setupWIF,
2532
2773
  slugifyKey,
2533
2774
  syncDriveImages,
2534
2775
  updateSpreadsheetWithLocalChanges,
@@ -0,0 +1,13 @@
1
+ /**
2
+ * gst-setup-wif – Interactive CLI for configuring Workload Identity Federation.
3
+ *
4
+ * Usage:
5
+ * npx -p @el-j/google-sheet-translations gst-setup-wif
6
+ * npx -p @el-j/google-sheet-translations gst-setup-wif \
7
+ * --project=my-gcp-project \
8
+ * --service-account=deploy@my-gcp-project.iam.gserviceaccount.com \
9
+ * --repo=myorg/myrepo \
10
+ * --key-file=./service-account-key.json
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/setup/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}