@mcpher/gas-fakes 2.0.11 → 2.0.13
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 +3 -1
- package/package.json +5 -1
- package/src/services/slidesapp/fakepageelement.js +14 -1
- package/src/services/slidesapp/fakeslide.js +116 -0
- package/src/services/slidesapp/faketable.js +63 -0
- package/src/services/slidesapp/faketablecell.js +51 -0
- package/src/services/slidesapp/faketablerow.js +49 -0
- package/src/services/slidesapp/faketextrange.js +2 -0
- package/src/services/slidesapp/pageelementfactory.js +4 -0
- package/src/support/auth.js +37 -47
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# <img src="./logo.png" alt="gas-fakes logo" width="50" align="top">
|
|
1
|
+
# <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Run Native Apps Script code anywhere with gas-fakes
|
|
2
2
|
|
|
3
3
|
I use clasp/antigravity to develop Google Apps Script (GAS) applications, but when using GAS native services, there's way too much back and forwards to the GAS IDE going while testing. I set myself the ambition of implementing a fake version of the GAS runtime environment on Node so I could at least do some testing and debugging of Apps Scripts locally on Node.
|
|
4
4
|
|
|
@@ -170,6 +170,8 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
170
170
|
- [gas fakes cli](gas-fakes-cli.md)
|
|
171
171
|
- [running gas-fakes on google cloud run](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
172
172
|
- [running gas-fakes on google kubernetes engine](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
173
|
+
- [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
174
|
+
- [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
|
|
173
175
|
- [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
|
174
176
|
- [Inside the volatile world of a Google Document](https://ramblings.mcpher.com/inside-the-volatile-world-of-a-google-document/)
|
|
175
177
|
- [Apps Script Services on Node – using apps script libraries](https://ramblings.mcpher.com/apps-script-services-on-node-using-apps-script-libraries/)
|
package/package.json
CHANGED
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"unzipper": "^0.12.3",
|
|
25
25
|
"zod": "^4.3.6"
|
|
26
26
|
},
|
|
27
|
+
"overrides": {
|
|
28
|
+
"minimatch": "^10.2.1",
|
|
29
|
+
"glob": "^13.0.0"
|
|
30
|
+
},
|
|
27
31
|
"type": "module",
|
|
28
32
|
"scripts": {
|
|
29
33
|
"pub": "npm publish --access public",
|
|
@@ -33,7 +37,7 @@
|
|
|
33
37
|
},
|
|
34
38
|
"name": "@mcpher/gas-fakes",
|
|
35
39
|
"author": "bruce mcpherson",
|
|
36
|
-
"version": "2.0.
|
|
40
|
+
"version": "2.0.13",
|
|
37
41
|
"license": "MIT",
|
|
38
42
|
"main": "main.js",
|
|
39
43
|
"description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
|
|
@@ -64,6 +64,18 @@ export class FakePageElement {
|
|
|
64
64
|
throw new Error('PageElement is not a line.');
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Returns the page element as a table.
|
|
69
|
+
* @returns {FakeTable} The table.
|
|
70
|
+
*/
|
|
71
|
+
asTable() {
|
|
72
|
+
if (this.__resource.table) {
|
|
73
|
+
const { newFakeTable } = PageElementRegistry;
|
|
74
|
+
return newFakeTable(this.__resource, this.__page);
|
|
75
|
+
}
|
|
76
|
+
throw new Error('PageElement is not a table.');
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
/**
|
|
68
80
|
* Gets the type of the page element.
|
|
69
81
|
* @returns {SlidesApp.PageElementType} The type.
|
|
@@ -391,5 +403,6 @@ export class FakePageElement {
|
|
|
391
403
|
|
|
392
404
|
export const PageElementRegistry = {
|
|
393
405
|
newFakeShape: null,
|
|
394
|
-
newFakeLine: null
|
|
406
|
+
newFakeLine: null,
|
|
407
|
+
newFakeTable: null
|
|
395
408
|
};
|
|
@@ -167,6 +167,23 @@ export class FakeSlide {
|
|
|
167
167
|
return newElement.asShape();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Inserts a text box.
|
|
172
|
+
* @param {string} text The text to insert.
|
|
173
|
+
* @param {number} left The left position.
|
|
174
|
+
* @param {number} top The top position.
|
|
175
|
+
* @param {number} width The width.
|
|
176
|
+
* @param {number} height The height.
|
|
177
|
+
* @returns {FakeShape} The new text box.
|
|
178
|
+
*/
|
|
179
|
+
insertTextBox(text, left, top, width, height) {
|
|
180
|
+
const shape = this.insertShape('TEXT_BOX', left, top, width, height);
|
|
181
|
+
if (text) {
|
|
182
|
+
shape.getText().setText(text);
|
|
183
|
+
}
|
|
184
|
+
return shape;
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
/**
|
|
171
188
|
* Inserts a line.
|
|
172
189
|
* @param {SlidesApp.LineCategory} lineCategory The line category.
|
|
@@ -212,6 +229,105 @@ export class FakeSlide {
|
|
|
212
229
|
return newElement.asLine();
|
|
213
230
|
}
|
|
214
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Inserts a table.
|
|
234
|
+
* @param {number|FakeTable} rowsOrTable The number of rows or a table to copy.
|
|
235
|
+
* @param {number} [columns] The number of columns (if rowsOrTable is a number).
|
|
236
|
+
* @param {number} [left]
|
|
237
|
+
* @param {number} [top]
|
|
238
|
+
* @param {number} [width]
|
|
239
|
+
* @param {number} [height]
|
|
240
|
+
* @returns {FakeTable} The new table.
|
|
241
|
+
*/
|
|
242
|
+
insertTable(rowsOrTable, columns, left = 0, top = 0, width = 300, height = 300) {
|
|
243
|
+
const presentationId = this.__presentation.getId();
|
|
244
|
+
const objectId = `table_${Math.random().toString(36).substring(2, 11)}`;
|
|
245
|
+
let request = null;
|
|
246
|
+
|
|
247
|
+
if (typeof rowsOrTable === 'number') {
|
|
248
|
+
request = {
|
|
249
|
+
createTable: {
|
|
250
|
+
objectId,
|
|
251
|
+
rows: rowsOrTable,
|
|
252
|
+
columns: columns,
|
|
253
|
+
elementProperties: {
|
|
254
|
+
pageObjectId: this.getObjectId(),
|
|
255
|
+
size: {
|
|
256
|
+
width: { magnitude: width, unit: 'PT' },
|
|
257
|
+
height: { magnitude: height, unit: 'PT' }
|
|
258
|
+
},
|
|
259
|
+
transform: {
|
|
260
|
+
scaleX: 1,
|
|
261
|
+
scaleY: 1,
|
|
262
|
+
translateX: left,
|
|
263
|
+
translateY: top,
|
|
264
|
+
unit: 'PT'
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
} else {
|
|
270
|
+
// Copy table logic - use duplicateObject if it's the same presentation?
|
|
271
|
+
// Or manually create new table with same rows/cols.
|
|
272
|
+
// Slide.insertTable(Table) usually means copy.
|
|
273
|
+
const table = rowsOrTable;
|
|
274
|
+
request = {
|
|
275
|
+
createTable: {
|
|
276
|
+
objectId,
|
|
277
|
+
rows: table.getNumRows(),
|
|
278
|
+
columns: table.getNumColumns(),
|
|
279
|
+
elementProperties: {
|
|
280
|
+
pageObjectId: this.getObjectId(),
|
|
281
|
+
size: {
|
|
282
|
+
width: { magnitude: table.getWidth(), unit: 'PT' },
|
|
283
|
+
height: { magnitude: table.getHeight(), unit: 'PT' }
|
|
284
|
+
},
|
|
285
|
+
transform: {
|
|
286
|
+
scaleX: 1,
|
|
287
|
+
scaleY: 1,
|
|
288
|
+
translateX: table.getLeft(),
|
|
289
|
+
translateY: table.getTop(),
|
|
290
|
+
unit: 'PT'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
Slides.Presentations.batchUpdate([request], presentationId);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (!err?.message?.includes('already exists')) throw err;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const elements = this.getPageElements();
|
|
304
|
+
const newElement = elements.find(e => e.getObjectId() === objectId);
|
|
305
|
+
if (!newElement) throw new Error('New table not found');
|
|
306
|
+
|
|
307
|
+
const newTable = newElement.asTable();
|
|
308
|
+
|
|
309
|
+
// If copying, we should probably copy cell contents too.
|
|
310
|
+
if (typeof rowsOrTable !== 'number') {
|
|
311
|
+
const sourceTable = rowsOrTable;
|
|
312
|
+
const targetTable = newTable;
|
|
313
|
+
const rows = sourceTable.getRows();
|
|
314
|
+
const targetRows = targetTable.getRows();
|
|
315
|
+
|
|
316
|
+
for (let r = 0; r < rows.length; r++) {
|
|
317
|
+
const cells = rows[r].getCells();
|
|
318
|
+
const targetCells = targetRows[r].getCells();
|
|
319
|
+
for (let c = 0; c < cells.length; c++) {
|
|
320
|
+
const text = cells[c].getText().asString();
|
|
321
|
+
if (text) {
|
|
322
|
+
targetCells[c].getText().setText(text);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return newTable;
|
|
329
|
+
}
|
|
330
|
+
|
|
215
331
|
duplicate() {
|
|
216
332
|
const presentationId = this.__presentation.getId();
|
|
217
333
|
const objectId = `slide_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { FakePageElement, PageElementRegistry } from './fakepageelement.js';
|
|
3
|
+
import { newFakeTableRow } from './faketablerow.js';
|
|
4
|
+
|
|
5
|
+
export const newFakeTable = (...args) => {
|
|
6
|
+
return Proxies.guard(new FakeTable(...args));
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
PageElementRegistry.newFakeTable = newFakeTable;
|
|
10
|
+
|
|
11
|
+
export class FakeTable extends FakePageElement {
|
|
12
|
+
constructor(resource, page) {
|
|
13
|
+
super(resource, page);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get __presentation() {
|
|
17
|
+
return this.__page.__presentation || this.__page.__slide?.__presentation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Gets the rows in the table.
|
|
22
|
+
* @returns {FakeTableRow[]} The rows.
|
|
23
|
+
*/
|
|
24
|
+
getRows() {
|
|
25
|
+
return (this.__resource.table?.tableRows || []).map((row, index) =>
|
|
26
|
+
newFakeTableRow(row, this, index)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets a row by its index.
|
|
32
|
+
* @param {number} index The row index.
|
|
33
|
+
* @returns {FakeTableRow} The row.
|
|
34
|
+
*/
|
|
35
|
+
getRow(index) {
|
|
36
|
+
const rows = this.getRows();
|
|
37
|
+
if (index < 0 || index >= rows.length) {
|
|
38
|
+
throw new Error(`Row index ${index} out of bounds`);
|
|
39
|
+
}
|
|
40
|
+
return rows[index];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets the number of rows in the table.
|
|
45
|
+
* @returns {number} The number of rows.
|
|
46
|
+
*/
|
|
47
|
+
getNumRows() {
|
|
48
|
+
return (this.__resource.table?.tableRows || []).length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Gets the number of columns in the table.
|
|
53
|
+
* @returns {number} The number of columns.
|
|
54
|
+
*/
|
|
55
|
+
getNumColumns() {
|
|
56
|
+
const rows = this.__resource.table?.tableRows || [];
|
|
57
|
+
return rows.length > 0 ? (rows[0].tableCells || []).length : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toString() {
|
|
61
|
+
return 'Table';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeTextRange } from './faketextrange.js';
|
|
3
|
+
|
|
4
|
+
export const newFakeTableCell = (...args) => {
|
|
5
|
+
return Proxies.guard(new FakeTableCell(...args));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class FakeTableCell {
|
|
9
|
+
constructor(resource, table, rowIndex, colIndex) {
|
|
10
|
+
this.__resource = resource;
|
|
11
|
+
this.__table = table;
|
|
12
|
+
this.__rowIndex = rowIndex;
|
|
13
|
+
this.__colIndex = colIndex;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Gets the text in the cell.
|
|
18
|
+
* @returns {FakeTextRange} The text range.
|
|
19
|
+
*/
|
|
20
|
+
getText() {
|
|
21
|
+
// FakeTableCell in Slides API doesn't have a direct shape resource to pass to TextRange?
|
|
22
|
+
// Actually, TableCell has text property in Slides API.
|
|
23
|
+
// Wait, let's check Slides API TableCell resource.
|
|
24
|
+
// It has `text` field of type `TextContent`.
|
|
25
|
+
// FakeTextRange expects a `shape` with `__resource.shape.text`.
|
|
26
|
+
// We might need to adapt FakeTextRange or mock a shape-like object.
|
|
27
|
+
|
|
28
|
+
// Let's create a proxy for shape so FakeTextRange can work.
|
|
29
|
+
const mockShape = {
|
|
30
|
+
getObjectId: () => this.__table.getObjectId(),
|
|
31
|
+
__resource: {
|
|
32
|
+
shape: {
|
|
33
|
+
text: this.__resource.text || { textElements: [] }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
__cellLocation: {
|
|
37
|
+
rowIndex: this.__rowIndex,
|
|
38
|
+
columnIndex: this.__colIndex
|
|
39
|
+
},
|
|
40
|
+
__presentation: this.__table.__presentation
|
|
41
|
+
};
|
|
42
|
+
if (!this.__resource.text) {
|
|
43
|
+
this.__resource.text = mockShape.__resource.shape.text;
|
|
44
|
+
}
|
|
45
|
+
return newFakeTextRange(mockShape);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toString() {
|
|
49
|
+
return 'TableCell';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeTableCell } from './faketablecell.js';
|
|
3
|
+
|
|
4
|
+
export const newFakeTableRow = (...args) => {
|
|
5
|
+
return Proxies.guard(new FakeTableRow(...args));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class FakeTableRow {
|
|
9
|
+
constructor(resource, table, rowIndex) {
|
|
10
|
+
this.__resource = resource;
|
|
11
|
+
this.__table = table;
|
|
12
|
+
this.__rowIndex = rowIndex;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gets the cells in the row.
|
|
17
|
+
* @returns {FakeTableCell[]} The cells.
|
|
18
|
+
*/
|
|
19
|
+
getCells() {
|
|
20
|
+
return (this.__resource.tableCells || []).map((cell, colIndex) =>
|
|
21
|
+
newFakeTableCell(cell, this.__table, this.__rowIndex, colIndex)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gets a cell by its index.
|
|
27
|
+
* @param {number} index The cell index.
|
|
28
|
+
* @returns {FakeTableCell} The cell.
|
|
29
|
+
*/
|
|
30
|
+
getCell(index) {
|
|
31
|
+
const cells = this.getCells();
|
|
32
|
+
if (index < 0 || index >= cells.length) {
|
|
33
|
+
throw new Error(`Cell index ${index} out of bounds`);
|
|
34
|
+
}
|
|
35
|
+
return cells[index];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the number of cells in the row.
|
|
40
|
+
* @returns {number} The number of cells.
|
|
41
|
+
*/
|
|
42
|
+
getNumCells() {
|
|
43
|
+
return (this.__resource.tableCells || []).length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toString() {
|
|
47
|
+
return 'TableRow';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -83,6 +83,7 @@ export class FakeTextRange {
|
|
|
83
83
|
requests.push({
|
|
84
84
|
deleteText: {
|
|
85
85
|
objectId: objectId,
|
|
86
|
+
cellLocation: this.__shape.__cellLocation,
|
|
86
87
|
textRange: {
|
|
87
88
|
type: 'FROM_START_INDEX',
|
|
88
89
|
startIndex: 0
|
|
@@ -104,6 +105,7 @@ export class FakeTextRange {
|
|
|
104
105
|
requests.push({
|
|
105
106
|
insertText: {
|
|
106
107
|
objectId: objectId,
|
|
108
|
+
cellLocation: this.__shape.__cellLocation,
|
|
107
109
|
insertionIndex: 0,
|
|
108
110
|
text: newText
|
|
109
111
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { newFakeShape } from './fakeshape.js';
|
|
2
2
|
import { newFakeLine } from './fakeline.js';
|
|
3
|
+
import { newFakeTable } from './faketable.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Converts a base PageElement to a more specific subclass (Shape, Line, etc.)
|
|
@@ -14,6 +15,9 @@ export const asSpecificPageElement = (pageElement) => {
|
|
|
14
15
|
if (resource.line) {
|
|
15
16
|
return newFakeLine(resource, pageElement.__page);
|
|
16
17
|
}
|
|
18
|
+
if (resource.table) {
|
|
19
|
+
return newFakeTable(resource, pageElement.__page);
|
|
20
|
+
}
|
|
17
21
|
// Add other types as they are implemented
|
|
18
22
|
return pageElement;
|
|
19
23
|
};
|
package/src/support/auth.js
CHANGED
|
@@ -56,7 +56,25 @@ const getHashedUserId = () =>
|
|
|
56
56
|
const _getTokenInfo = async (client) => {
|
|
57
57
|
const tokenResponse = await client.getAccessToken();
|
|
58
58
|
const token = tokenResponse.token;
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
let tokenInfo;
|
|
61
|
+
if (typeof client.getTokenInfo === 'function') {
|
|
62
|
+
tokenInfo = await client.getTokenInfo(token);
|
|
63
|
+
} else {
|
|
64
|
+
// Fallback for clients like AwsClient that don't have getTokenInfo
|
|
65
|
+
// Call the Token Info API directly
|
|
66
|
+
const response = await client.request({
|
|
67
|
+
url: `https://oauth2.googleapis.com/tokeninfo?access_token=${token}`,
|
|
68
|
+
method: 'GET'
|
|
69
|
+
});
|
|
70
|
+
tokenInfo = response.data;
|
|
71
|
+
|
|
72
|
+
// Ensure email is populated from subject if missing (WIF fallback)
|
|
73
|
+
if (!tokenInfo.email && process.env.GOOGLE_WORKSPACE_SUBJECT) {
|
|
74
|
+
tokenInfo.email = process.env.GOOGLE_WORKSPACE_SUBJECT;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
return {
|
|
61
79
|
tokenInfo,
|
|
62
80
|
token
|
|
@@ -112,9 +130,6 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
112
130
|
mayLog(`...discovered project ID: ${_projectId}`)
|
|
113
131
|
|
|
114
132
|
// steering for auth type
|
|
115
|
-
// 1. if AUTH_TYPE is DWD, use DWD
|
|
116
|
-
// 2. if AUTH_TYPE is ADC, use ADC
|
|
117
|
-
// 3. if AUTH_TYPE is not set, use DWD if saName is present, else ADC
|
|
118
133
|
const saName = process.env.GOOGLE_SERVICE_ACCOUNT_NAME
|
|
119
134
|
const authType = process.env.AUTH_TYPE?.toLowerCase()
|
|
120
135
|
const useDwd = authType === 'dwd' || (authType !== 'adc' && saName)
|
|
@@ -134,19 +149,21 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
134
149
|
mayLog(`...attempting to use service account: ${targetPrincipal}`)
|
|
135
150
|
|
|
136
151
|
/// _sourceClient is the identity of the person/thing running the code
|
|
137
|
-
// we'll try to get the openid and email scopes for the source client too if they are in the manifest
|
|
138
152
|
const sourceScopes = scopes.filter(s => s === 'openid' || s === 'https://www.googleapis.com/auth/userinfo.email')
|
|
139
153
|
_sourceClient = await _auth.getClient(sourceScopes.length > 0 ? { scopes: sourceScopes } : {})
|
|
140
154
|
|
|
141
155
|
// now to get who the real user is
|
|
142
156
|
const { tokenInfo: userInfo } = await getSourceAccessTokenInfo()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// DWD Subject priority:
|
|
146
|
-
// 1. GOOGLE_WORKSPACE_SUBJECT
|
|
147
|
-
// 2. Caller identity (from getSourceAccessTokenInfo)
|
|
157
|
+
|
|
158
|
+
// AWS tokens might not have email info
|
|
148
159
|
const saEmail = targetPrincipal
|
|
149
160
|
const userEmail = process.env.GOOGLE_WORKSPACE_SUBJECT || userInfo.email
|
|
161
|
+
|
|
162
|
+
if (userEmail) {
|
|
163
|
+
mayLog(`...user verified as: ${userEmail}`);
|
|
164
|
+
} else {
|
|
165
|
+
mayLog(`...warning: user identity could not be verified from token, using fallback`);
|
|
166
|
+
}
|
|
150
167
|
|
|
151
168
|
const dwdClient = new OAuth2Client()
|
|
152
169
|
dwdClient._token = null
|
|
@@ -168,10 +185,7 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
168
185
|
scope: scopes.join(' ')
|
|
169
186
|
}
|
|
170
187
|
|
|
171
|
-
//mayLog(`[Auth] DWD Scopes: ${payload.scope}`)
|
|
172
|
-
|
|
173
188
|
// Sign the JWT via IAM API
|
|
174
|
-
// Note: The caller must have 'Service Account Token Creator' role on the target SA
|
|
175
189
|
const signUrl = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${saEmail}:signJwt`
|
|
176
190
|
const signResponse = await _sourceClient.request({
|
|
177
191
|
url: signUrl,
|
|
@@ -196,8 +210,6 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
196
210
|
|
|
197
211
|
if (!tokenResponse.ok) {
|
|
198
212
|
const errorText = await tokenResponse.text()
|
|
199
|
-
console.log ('... it looks like you forgot to enable domain-wide delegation for the service account')
|
|
200
|
-
console.log ('... rerun gas-fakes auth and check the instructions about enabling domain-wide delegation')
|
|
201
213
|
throw new Error(`Failed to exchange JWT for token: ${errorText}`)
|
|
202
214
|
}
|
|
203
215
|
|
|
@@ -209,10 +221,9 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
209
221
|
return { token: this._token }
|
|
210
222
|
}
|
|
211
223
|
|
|
212
|
-
// override request
|
|
224
|
+
// override request
|
|
213
225
|
const originalRequest = dwdClient.request.bind(dwdClient)
|
|
214
226
|
dwdClient.request = async function (options) {
|
|
215
|
-
// Ensure token is fresh
|
|
216
227
|
await this.getAccessToken()
|
|
217
228
|
return originalRequest(options)
|
|
218
229
|
}
|
|
@@ -230,14 +241,18 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
230
241
|
|
|
231
242
|
mayLog(`...using Domain-Wide Delegation for user: ${userEmail}`)
|
|
232
243
|
|
|
233
|
-
// check we can get an access token
|
|
234
|
-
const { tokenInfo } = await getAccessTokenInfo()
|
|
235
|
-
|
|
244
|
+
// check we can get an access token
|
|
245
|
+
const { tokenInfo: authedTokenInfo } = await getAccessTokenInfo()
|
|
246
|
+
if (authedTokenInfo.email) {
|
|
247
|
+
mayLog(`...sa (acting as user) verified as: ${authedTokenInfo.email}`);
|
|
248
|
+
} else {
|
|
249
|
+
mayLog(`...sa (acting as user) token acquired successfully`);
|
|
250
|
+
}
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
|
|
239
254
|
} catch (error) {
|
|
240
|
-
mayLog(`...auth failed - check
|
|
255
|
+
mayLog(`...auth failed - check environment variables and workload identity: ${error}`)
|
|
241
256
|
throw error
|
|
242
257
|
}
|
|
243
258
|
return getAuth()
|
|
@@ -256,15 +271,6 @@ const invalidateToken = () => {
|
|
|
256
271
|
}
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
|
-
/**
|
|
260
|
-
* we'll be using adc credentials so no need for any special auth here
|
|
261
|
-
* the idea here is to keep addign scopes to any auth so we have them all
|
|
262
|
-
* @param {string[]} [scopes=[]] the required scopes will be added to existing scopes already asked for
|
|
263
|
-
* @param {string} [keyFile=null]
|
|
264
|
-
* @param {boolean} [mcpLoading=false] When the MCP server is loading, this value is true. By this, the invalid values can be hidden while the MCP server is loading. This is important for using Google Antigravity.
|
|
265
|
-
* @returns {GoogleAuth.auth}
|
|
266
|
-
*/
|
|
267
|
-
|
|
268
274
|
|
|
269
275
|
/**
|
|
270
276
|
* if we're doing a fetch on drive API we need a special header
|
|
@@ -278,10 +284,6 @@ const googify = (options = {}) => {
|
|
|
278
284
|
// if no authorization, we dont need this either
|
|
279
285
|
if (!Reflect.has(headers, "Authorization")) return options;
|
|
280
286
|
|
|
281
|
-
// we'll need the projectID for this
|
|
282
|
-
// note - you must add the x-goog-user-project header, otherwise it'll use some nonexistent project
|
|
283
|
-
// see https://cloud.google.com/docs/authentication/rest#set-billing-project
|
|
284
|
-
// this has been syncified
|
|
285
287
|
const projectId = getProjectId();
|
|
286
288
|
return {
|
|
287
289
|
...options,
|
|
@@ -293,7 +295,6 @@ const googify = (options = {}) => {
|
|
|
293
295
|
};
|
|
294
296
|
|
|
295
297
|
/**
|
|
296
|
-
* this would have been set up when manifest was imported
|
|
297
298
|
* @returns {string} the project id
|
|
298
299
|
*/
|
|
299
300
|
const getProjectId = () => {
|
|
@@ -318,7 +319,6 @@ const getAuth = () => {
|
|
|
318
319
|
if (!hasAuth())
|
|
319
320
|
throw new Error(`auth hasnt been intialized with setAuth yet`);
|
|
320
321
|
|
|
321
|
-
// Simply return the client we've already prepared/patched
|
|
322
322
|
return getAuthClient();
|
|
323
323
|
};
|
|
324
324
|
|
|
@@ -326,17 +326,11 @@ const getAuth = () => {
|
|
|
326
326
|
|
|
327
327
|
/**
|
|
328
328
|
* why is this here ?
|
|
329
|
-
* because when we syncit, we import auth for each method and it needs this
|
|
330
|
-
* if it was somewhere else we'd need to import that too.
|
|
331
|
-
* we can't serialize a return object
|
|
332
|
-
* so we just select a few props from it
|
|
333
|
-
* @param {SyncApiResponse} result
|
|
334
|
-
* @returns
|
|
335
329
|
*/
|
|
336
330
|
export const responseSyncify = (result) => {
|
|
337
331
|
if (!result) {
|
|
338
332
|
return {
|
|
339
|
-
status: 503,
|
|
333
|
+
status: 503,
|
|
340
334
|
statusCode: 503,
|
|
341
335
|
statusText: "Worker Error: No response object received from API call",
|
|
342
336
|
error: {
|
|
@@ -357,10 +351,6 @@ export const responseSyncify = (result) => {
|
|
|
357
351
|
};
|
|
358
352
|
};
|
|
359
353
|
|
|
360
|
-
/**
|
|
361
|
-
* these are the ones that have been so far requested
|
|
362
|
-
* @returns {Set}
|
|
363
|
-
*/
|
|
364
354
|
const getAuthedScopes = () => _authScopes;
|
|
365
355
|
|
|
366
356
|
export const Auth = {
|