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

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,226 @@ 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
+ const opUrl = operationName.startsWith("http") ? operationName : `https://iam.googleapis.com/v1/${operationName}`;
2446
+ const deadline = Date.now() + maxWaitMs;
2447
+ const maxWaitSecs = Math.round(maxWaitMs / 1e3);
2448
+ while (Date.now() < deadline) {
2449
+ const op = await gcpFetch(opUrl, token);
2450
+ if (op.done) {
2451
+ if (op.error) {
2452
+ throw new Error(`Operation failed: ${op.error.message}`);
2453
+ }
2454
+ return;
2455
+ }
2456
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2457
+ }
2458
+ if (Date.now() >= deadline) {
2459
+ throw new Error(
2460
+ `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).`
2461
+ );
2462
+ }
2463
+ }
2464
+ async function getProjectNumber(projectId, token) {
2465
+ const data = await gcpFetch(
2466
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(projectId)}`,
2467
+ token
2468
+ );
2469
+ return data.projectNumber;
2470
+ }
2471
+ async function createOrGetWifPool(projectId, poolId, token) {
2472
+ try {
2473
+ const op = await gcpFetch(
2474
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools?workloadIdentityPoolId=${encodeURIComponent(poolId)}`,
2475
+ token,
2476
+ "POST",
2477
+ {
2478
+ displayName: "GitHub Actions Pool",
2479
+ description: "Pool for GitHub Actions OIDC authentication",
2480
+ disabled: false
2481
+ }
2482
+ );
2483
+ if (!op.done) {
2484
+ await pollOperation(op.name, token);
2485
+ } else if (op.error) {
2486
+ throw new Error(`Operation failed: ${op.error.message}`);
2487
+ }
2488
+ } catch (err) {
2489
+ if (err instanceof GcpApiError && err.status === 409) {
2490
+ return;
2491
+ }
2492
+ throw err;
2493
+ }
2494
+ }
2495
+ async function createOrGetWifProvider(projectId, poolId, providerId, githubRepo, token) {
2496
+ try {
2497
+ const op = await gcpFetch(
2498
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools/${encodeURIComponent(poolId)}/providers?workloadIdentityPoolProviderId=${encodeURIComponent(providerId)}`,
2499
+ token,
2500
+ "POST",
2501
+ {
2502
+ displayName: "GitHub OIDC Provider",
2503
+ disabled: false,
2504
+ attributeMapping: {
2505
+ "google.subject": "assertion.sub",
2506
+ "attribute.actor": "assertion.actor",
2507
+ "attribute.repository": "assertion.repository"
2508
+ },
2509
+ // Scope the provider to this exact repository for security
2510
+ attributeCondition: `assertion.repository=='${githubRepo}'`,
2511
+ oidc: {
2512
+ issuerUri: "https://token.actions.githubusercontent.com"
2513
+ }
2514
+ }
2515
+ );
2516
+ if (!op.done) {
2517
+ await pollOperation(op.name, token);
2518
+ } else if (op.error) {
2519
+ throw new Error(`Operation failed: ${op.error.message}`);
2520
+ }
2521
+ } catch (err) {
2522
+ if (err instanceof GcpApiError && err.status === 409) {
2523
+ return;
2524
+ }
2525
+ throw err;
2526
+ }
2527
+ }
2528
+ async function bindServiceAccount(projectId, serviceAccountEmail, projectNumber, poolId, githubRepo, token) {
2529
+ const saResource = `projects/${encodeURIComponent(projectId)}/serviceAccounts/${encodeURIComponent(serviceAccountEmail)}`;
2530
+ const principal = `principalSet://iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/attribute.repository/${githubRepo}`;
2531
+ const policy = await gcpFetch(
2532
+ `https://iam.googleapis.com/v1/${saResource}:getIamPolicy`,
2533
+ token,
2534
+ "POST",
2535
+ {}
2536
+ );
2537
+ const bindings = policy.bindings ?? [];
2538
+ const role = "roles/iam.workloadIdentityUser";
2539
+ const existing = bindings.find((b) => b.role === role);
2540
+ if (existing) {
2541
+ if (existing.members.includes(principal)) {
2542
+ return;
2543
+ }
2544
+ existing.members.push(principal);
2545
+ } else {
2546
+ bindings.push({ role, members: [principal] });
2547
+ }
2548
+ await gcpFetch(
2549
+ `https://iam.googleapis.com/v1/${saResource}:setIamPolicy`,
2550
+ token,
2551
+ "POST",
2552
+ { policy: { ...policy, bindings } }
2553
+ );
2554
+ }
2555
+ async function setupWIF(options) {
2556
+ const poolId = options.poolId ?? "github-actions";
2557
+ const providerId = options.providerId ?? "github-oidc";
2558
+ const log = options.onProgress ?? (() => void 0);
2559
+ if (!options.githubRepo.includes("/")) {
2560
+ throw new Error(
2561
+ `githubRepo must be in "owner/repo" format, got: "${options.githubRepo}"`
2562
+ );
2563
+ }
2564
+ log("Authenticating with Google Cloud...");
2565
+ const token = await getAccessToken4(options.keyFilePath);
2566
+ log("Fetching project number...");
2567
+ const projectNumber = await getProjectNumber(options.projectId, token);
2568
+ log(`Creating Workload Identity Pool "${poolId}"...`);
2569
+ await createOrGetWifPool(options.projectId, poolId, token);
2570
+ log(`Creating OIDC Provider "${providerId}"...`);
2571
+ await createOrGetWifProvider(
2572
+ options.projectId,
2573
+ poolId,
2574
+ providerId,
2575
+ options.githubRepo,
2576
+ token
2577
+ );
2578
+ log("Binding service account permissions...");
2579
+ await bindServiceAccount(
2580
+ options.projectId,
2581
+ options.serviceAccountEmail,
2582
+ projectNumber,
2583
+ poolId,
2584
+ options.githubRepo,
2585
+ token
2586
+ );
2587
+ const wifProvider = `projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/providers/${providerId}`;
2588
+ return { wifProvider, projectNumber, poolId, providerId };
2589
+ }
2590
+ async function grantDrivePermissions(options) {
2591
+ const token = await getAccessToken4(options.keyFilePath);
2592
+ const policy = await gcpFetch(
2593
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:getIamPolicy`,
2594
+ token,
2595
+ "POST",
2596
+ {}
2597
+ );
2598
+ const bindings = policy.bindings ?? [];
2599
+ const role = "roles/drive.file";
2600
+ const member = `serviceAccount:${options.serviceAccountEmail}`;
2601
+ const existing = bindings.find((b) => b.role === role);
2602
+ if (existing) {
2603
+ if (existing.members.includes(member)) {
2604
+ return;
2605
+ }
2606
+ existing.members.push(member);
2607
+ } else {
2608
+ bindings.push({ role, members: [member] });
2609
+ }
2610
+ await gcpFetch(
2611
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:setIamPolicy`,
2612
+ token,
2613
+ "POST",
2614
+ { policy: { ...policy, bindings } }
2615
+ );
2616
+ }
2617
+
2401
2618
  // src/index.ts
2402
2619
  var index_default = getSpreadSheetData;
2403
2620
  export {
2404
2621
  DEFAULT_IMAGE_EXTENSIONS,
2405
2622
  DEFAULT_WAIT_SECONDS,
2623
+ GcpApiError,
2406
2624
  buildGoogleAuth,
2407
2625
  buildManifest,
2408
2626
  convertFromDataJsonFormat,
@@ -2424,6 +2642,7 @@ export {
2424
2642
  getOriginalHeaderForLocale,
2425
2643
  getSpreadSheetData,
2426
2644
  getTranslationSummary,
2645
+ grantDrivePermissions,
2427
2646
  handleBidirectionalSync,
2428
2647
  inferLocaleFromDocName,
2429
2648
  ingestDoc,
@@ -2441,6 +2660,7 @@ export {
2441
2660
  resolveLocaleWithFallback,
2442
2661
  scanDriveFolderForDocs,
2443
2662
  scanDriveFolderForSpreadsheets,
2663
+ setupWIF,
2444
2664
  slugifyKey,
2445
2665
  syncDriveImages,
2446
2666
  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,227 @@ 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
+ const opUrl = operationName.startsWith("http") ? operationName : `https://iam.googleapis.com/v1/${operationName}`;
2537
+ const deadline = Date.now() + maxWaitMs;
2538
+ const maxWaitSecs = Math.round(maxWaitMs / 1e3);
2539
+ while (Date.now() < deadline) {
2540
+ const op = await gcpFetch(opUrl, token);
2541
+ if (op.done) {
2542
+ if (op.error) {
2543
+ throw new Error(`Operation failed: ${op.error.message}`);
2544
+ }
2545
+ return;
2546
+ }
2547
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2548
+ }
2549
+ if (Date.now() >= deadline) {
2550
+ throw new Error(
2551
+ `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).`
2552
+ );
2553
+ }
2554
+ }
2555
+ async function getProjectNumber(projectId, token) {
2556
+ const data = await gcpFetch(
2557
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(projectId)}`,
2558
+ token
2559
+ );
2560
+ return data.projectNumber;
2561
+ }
2562
+ async function createOrGetWifPool(projectId, poolId, token) {
2563
+ try {
2564
+ const op = await gcpFetch(
2565
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools?workloadIdentityPoolId=${encodeURIComponent(poolId)}`,
2566
+ token,
2567
+ "POST",
2568
+ {
2569
+ displayName: "GitHub Actions Pool",
2570
+ description: "Pool for GitHub Actions OIDC authentication",
2571
+ disabled: false
2572
+ }
2573
+ );
2574
+ if (!op.done) {
2575
+ await pollOperation(op.name, token);
2576
+ } else if (op.error) {
2577
+ throw new Error(`Operation failed: ${op.error.message}`);
2578
+ }
2579
+ } catch (err) {
2580
+ if (err instanceof GcpApiError && err.status === 409) {
2581
+ return;
2582
+ }
2583
+ throw err;
2584
+ }
2585
+ }
2586
+ async function createOrGetWifProvider(projectId, poolId, providerId, githubRepo, token) {
2587
+ try {
2588
+ const op = await gcpFetch(
2589
+ `https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools/${encodeURIComponent(poolId)}/providers?workloadIdentityPoolProviderId=${encodeURIComponent(providerId)}`,
2590
+ token,
2591
+ "POST",
2592
+ {
2593
+ displayName: "GitHub OIDC Provider",
2594
+ disabled: false,
2595
+ attributeMapping: {
2596
+ "google.subject": "assertion.sub",
2597
+ "attribute.actor": "assertion.actor",
2598
+ "attribute.repository": "assertion.repository"
2599
+ },
2600
+ // Scope the provider to this exact repository for security
2601
+ attributeCondition: `assertion.repository=='${githubRepo}'`,
2602
+ oidc: {
2603
+ issuerUri: "https://token.actions.githubusercontent.com"
2604
+ }
2605
+ }
2606
+ );
2607
+ if (!op.done) {
2608
+ await pollOperation(op.name, token);
2609
+ } else if (op.error) {
2610
+ throw new Error(`Operation failed: ${op.error.message}`);
2611
+ }
2612
+ } catch (err) {
2613
+ if (err instanceof GcpApiError && err.status === 409) {
2614
+ return;
2615
+ }
2616
+ throw err;
2617
+ }
2618
+ }
2619
+ async function bindServiceAccount(projectId, serviceAccountEmail, projectNumber, poolId, githubRepo, token) {
2620
+ const saResource = `projects/${encodeURIComponent(projectId)}/serviceAccounts/${encodeURIComponent(serviceAccountEmail)}`;
2621
+ const principal = `principalSet://iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/attribute.repository/${githubRepo}`;
2622
+ const policy = await gcpFetch(
2623
+ `https://iam.googleapis.com/v1/${saResource}:getIamPolicy`,
2624
+ token,
2625
+ "POST",
2626
+ {}
2627
+ );
2628
+ const bindings = policy.bindings ?? [];
2629
+ const role = "roles/iam.workloadIdentityUser";
2630
+ const existing = bindings.find((b) => b.role === role);
2631
+ if (existing) {
2632
+ if (existing.members.includes(principal)) {
2633
+ return;
2634
+ }
2635
+ existing.members.push(principal);
2636
+ } else {
2637
+ bindings.push({ role, members: [principal] });
2638
+ }
2639
+ await gcpFetch(
2640
+ `https://iam.googleapis.com/v1/${saResource}:setIamPolicy`,
2641
+ token,
2642
+ "POST",
2643
+ { policy: { ...policy, bindings } }
2644
+ );
2645
+ }
2646
+ async function setupWIF(options) {
2647
+ const poolId = options.poolId ?? "github-actions";
2648
+ const providerId = options.providerId ?? "github-oidc";
2649
+ const log = options.onProgress ?? (() => void 0);
2650
+ if (!options.githubRepo.includes("/")) {
2651
+ throw new Error(
2652
+ `githubRepo must be in "owner/repo" format, got: "${options.githubRepo}"`
2653
+ );
2654
+ }
2655
+ log("Authenticating with Google Cloud...");
2656
+ const token = await getAccessToken4(options.keyFilePath);
2657
+ log("Fetching project number...");
2658
+ const projectNumber = await getProjectNumber(options.projectId, token);
2659
+ log(`Creating Workload Identity Pool "${poolId}"...`);
2660
+ await createOrGetWifPool(options.projectId, poolId, token);
2661
+ log(`Creating OIDC Provider "${providerId}"...`);
2662
+ await createOrGetWifProvider(
2663
+ options.projectId,
2664
+ poolId,
2665
+ providerId,
2666
+ options.githubRepo,
2667
+ token
2668
+ );
2669
+ log("Binding service account permissions...");
2670
+ await bindServiceAccount(
2671
+ options.projectId,
2672
+ options.serviceAccountEmail,
2673
+ projectNumber,
2674
+ poolId,
2675
+ options.githubRepo,
2676
+ token
2677
+ );
2678
+ const wifProvider = `projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/providers/${providerId}`;
2679
+ return { wifProvider, projectNumber, poolId, providerId };
2680
+ }
2681
+ async function grantDrivePermissions(options) {
2682
+ const token = await getAccessToken4(options.keyFilePath);
2683
+ const policy = await gcpFetch(
2684
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:getIamPolicy`,
2685
+ token,
2686
+ "POST",
2687
+ {}
2688
+ );
2689
+ const bindings = policy.bindings ?? [];
2690
+ const role = "roles/drive.file";
2691
+ const member = `serviceAccount:${options.serviceAccountEmail}`;
2692
+ const existing = bindings.find((b) => b.role === role);
2693
+ if (existing) {
2694
+ if (existing.members.includes(member)) {
2695
+ return;
2696
+ }
2697
+ existing.members.push(member);
2698
+ } else {
2699
+ bindings.push({ role, members: [member] });
2700
+ }
2701
+ await gcpFetch(
2702
+ `https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:setIamPolicy`,
2703
+ token,
2704
+ "POST",
2705
+ { policy: { ...policy, bindings } }
2706
+ );
2707
+ }
2708
+
2489
2709
  // src/index.ts
2490
2710
  var index_default = getSpreadSheetData;
2491
2711
  // Annotate the CommonJS export names for ESM import in node:
2492
2712
  0 && (module.exports = {
2493
2713
  DEFAULT_IMAGE_EXTENSIONS,
2494
2714
  DEFAULT_WAIT_SECONDS,
2715
+ GcpApiError,
2495
2716
  buildGoogleAuth,
2496
2717
  buildManifest,
2497
2718
  convertFromDataJsonFormat,
@@ -2512,6 +2733,7 @@ var index_default = getSpreadSheetData;
2512
2733
  getOriginalHeaderForLocale,
2513
2734
  getSpreadSheetData,
2514
2735
  getTranslationSummary,
2736
+ grantDrivePermissions,
2515
2737
  handleBidirectionalSync,
2516
2738
  inferLocaleFromDocName,
2517
2739
  ingestDoc,
@@ -2529,6 +2751,7 @@ var index_default = getSpreadSheetData;
2529
2751
  resolveLocaleWithFallback,
2530
2752
  scanDriveFolderForDocs,
2531
2753
  scanDriveFolderForSpreadsheets,
2754
+ setupWIF,
2532
2755
  slugifyKey,
2533
2756
  syncDriveImages,
2534
2757
  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"}
@@ -0,0 +1,95 @@
1
+ export interface WifSetupOptions {
2
+ /** Google Cloud project ID (e.g. "my-gcp-project") */
3
+ projectId: string;
4
+ /** Service account email (e.g. "deploy@my-gcp-project.iam.gserviceaccount.com") */
5
+ serviceAccountEmail: string;
6
+ /** GitHub repository in "owner/repo" format (e.g. "myorg/myrepo") */
7
+ githubRepo: string;
8
+ /** Workload Identity Pool ID (default: "github-actions") */
9
+ poolId?: string;
10
+ /** OIDC Provider ID (default: "github-oidc") */
11
+ providerId?: string;
12
+ /** Path to a service account JSON key file used for bootstrapping.
13
+ * If omitted, Application Default Credentials (ADC) are used. */
14
+ keyFilePath?: string;
15
+ /** Optional callback invoked before each setup step for progress reporting. */
16
+ onProgress?: (step: string) => void;
17
+ }
18
+ export interface WifSetupResult {
19
+ /** Full WIF provider resource name – set this as the `WIF_PROVIDER` env var */
20
+ wifProvider: string;
21
+ /** Numeric Google Cloud project number */
22
+ projectNumber: string;
23
+ /** Workload Identity Pool ID used */
24
+ poolId: string;
25
+ /** OIDC Provider ID used */
26
+ providerId: string;
27
+ }
28
+ export interface GrantDrivePermissionsOptions {
29
+ /** Google Cloud project ID */
30
+ projectId: string;
31
+ /** Service account email to grant Drive access */
32
+ serviceAccountEmail: string;
33
+ /** Path to a service account JSON key file used for bootstrapping. */
34
+ keyFilePath?: string;
35
+ }
36
+ export declare class GcpApiError extends Error {
37
+ readonly status: number;
38
+ constructor(message: string, status: number);
39
+ }
40
+ /**
41
+ * Configures Workload Identity Federation (WIF) on Google Cloud so that
42
+ * GitHub Actions can authenticate without a long-lived service account key.
43
+ *
44
+ * This function is **idempotent**: re-running it when resources already exist
45
+ * is safe – existing pools, providers and IAM bindings are reused.
46
+ *
47
+ * Steps performed:
48
+ * 1. Resolves the numeric Google Cloud project number.
49
+ * 2. Creates (or reuses) a Workload Identity Pool named `poolId`.
50
+ * 3. Creates (or reuses) an OIDC Provider scoped to `githubRepo`.
51
+ * 4. Grants the service account the `roles/iam.workloadIdentityUser` role
52
+ * for tokens originating from `githubRepo`.
53
+ *
54
+ * The returned {@link WifSetupResult.wifProvider} is the full provider resource
55
+ * name that should be set as the `WIF_PROVIDER` environment variable / GitHub
56
+ * Actions environment variable.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * import { setupWIF } from '@el-j/google-sheet-translations';
61
+ *
62
+ * const result = await setupWIF({
63
+ * projectId: 'my-gcp-project',
64
+ * serviceAccountEmail: 'deploy@my-gcp-project.iam.gserviceaccount.com',
65
+ * githubRepo: 'myorg/myrepo',
66
+ * keyFilePath: './service-account-key.json',
67
+ * onProgress: (step) => console.log(' ⏳', step),
68
+ * });
69
+ *
70
+ * console.log('WIF_PROVIDER =', result.wifProvider);
71
+ * ```
72
+ */
73
+ export declare function setupWIF(options: WifSetupOptions): Promise<WifSetupResult>;
74
+ /**
75
+ * Grants the service account `roles/drive.file` on the Google Cloud project.
76
+ *
77
+ * This is required when using the Drive-folder features of
78
+ * `@el-j/google-sheet-translations` (e.g. `manageDriveTranslations()`)
79
+ * so the service account can create and access spreadsheets inside a shared
80
+ * Drive folder.
81
+ *
82
+ * This function is **idempotent** – it checks the existing policy before
83
+ * adding the binding.
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * await grantDrivePermissions({
88
+ * projectId: 'my-gcp-project',
89
+ * serviceAccountEmail: 'deploy@my-gcp-project.iam.gserviceaccount.com',
90
+ * keyFilePath: './service-account-key.json',
91
+ * });
92
+ * ```
93
+ */
94
+ export declare function grantDrivePermissions(options: GrantDrivePermissionsOptions): Promise<void>;
95
+ //# sourceMappingURL=wifSetup.d.ts.map