dsh-antigravity-auth 0.1.0
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/README.zh.md +159 -0
- package/cordis.patch.yml +12 -0
- package/lib/capability-lifecycle--tgGKajs.js +1896 -0
- package/lib/client.cjs +2123 -0
- package/lib/client.cjs.map +1 -0
- package/lib/image.js +564 -0
- package/lib/index.js +176 -0
- package/lib/invariant.js +2 -0
- package/lib/live-gate-runner.js +542 -0
- package/lib/live-gates.js +97 -0
- package/lib/llm-adapter.js +916 -0
- package/lib/media-admission.js +206 -0
- package/lib/private-failure-B9vzUkQ7.js +23 -0
- package/lib/private-transport-urF138qW.js +641 -0
- package/lib/private-transport.js +2 -0
- package/lib/project-context.js +133 -0
- package/lib/quota.js +283 -0
- package/lib/replay.js +156 -0
- package/lib/rpc-contract-BFWfdKKs.js +491 -0
- package/lib/rpc-contract.js +2 -0
- package/lib/safe-text-AlEyN8q_.js +12 -0
- package/lib/search.js +259 -0
- package/lib/status-D0-em3Ru.js +125 -0
- package/lib/types/auth-service.d.ts +64 -0
- package/lib/types/auth-service.d.ts.map +1 -0
- package/lib/types/auth-store.d.ts +45 -0
- package/lib/types/auth-store.d.ts.map +1 -0
- package/lib/types/bootstrap-service.d.ts +6 -0
- package/lib/types/bootstrap-service.d.ts.map +1 -0
- package/lib/types/capability-gates.d.ts +19 -0
- package/lib/types/capability-gates.d.ts.map +1 -0
- package/lib/types/capability-lifecycle.d.ts +26 -0
- package/lib/types/capability-lifecycle.d.ts.map +1 -0
- package/lib/types/client/AntigravityAuthSettings.d.ts +19 -0
- package/lib/types/client/AntigravityAuthSettings.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +18 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +121 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/styles.d.ts +4 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/credential-coordinator.d.ts +84 -0
- package/lib/types/credential-coordinator.d.ts.map +1 -0
- package/lib/types/image.d.ts +39 -0
- package/lib/types/image.d.ts.map +1 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +3 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/live-gate-attachments.d.ts +6 -0
- package/lib/types/live-gate-attachments.d.ts.map +1 -0
- package/lib/types/live-gate-auth.d.ts +5 -0
- package/lib/types/live-gate-auth.d.ts.map +1 -0
- package/lib/types/live-gate-image.d.ts +5 -0
- package/lib/types/live-gate-image.d.ts.map +1 -0
- package/lib/types/live-gate-llm.d.ts +10 -0
- package/lib/types/live-gate-llm.d.ts.map +1 -0
- package/lib/types/live-gate-outcome.d.ts +8 -0
- package/lib/types/live-gate-outcome.d.ts.map +1 -0
- package/lib/types/live-gate-runner.d.ts +15 -0
- package/lib/types/live-gate-runner.d.ts.map +1 -0
- package/lib/types/live-gate-search.d.ts +5 -0
- package/lib/types/live-gate-search.d.ts.map +1 -0
- package/lib/types/live-gate-video.d.ts +8 -0
- package/lib/types/live-gate-video.d.ts.map +1 -0
- package/lib/types/live-gates.d.ts +25 -0
- package/lib/types/live-gates.d.ts.map +1 -0
- package/lib/types/llm-adapter.d.ts +65 -0
- package/lib/types/llm-adapter.d.ts.map +1 -0
- package/lib/types/login-types.d.ts +36 -0
- package/lib/types/login-types.d.ts.map +1 -0
- package/lib/types/media-admission.d.ts +54 -0
- package/lib/types/media-admission.d.ts.map +1 -0
- package/lib/types/model-catalog.d.ts +19 -0
- package/lib/types/model-catalog.d.ts.map +1 -0
- package/lib/types/oauth-flow.d.ts +87 -0
- package/lib/types/oauth-flow.d.ts.map +1 -0
- package/lib/types/private-failure.d.ts +6 -0
- package/lib/types/private-failure.d.ts.map +1 -0
- package/lib/types/private-transport-error.d.ts +13 -0
- package/lib/types/private-transport-error.d.ts.map +1 -0
- package/lib/types/private-transport.d.ts +48 -0
- package/lib/types/private-transport.d.ts.map +1 -0
- package/lib/types/project-context.d.ts +30 -0
- package/lib/types/project-context.d.ts.map +1 -0
- package/lib/types/quota.d.ts +43 -0
- package/lib/types/quota.d.ts.map +1 -0
- package/lib/types/raw-http.d.ts +11 -0
- package/lib/types/raw-http.d.ts.map +1 -0
- package/lib/types/replay.d.ts +30 -0
- package/lib/types/replay.d.ts.map +1 -0
- package/lib/types/rpc-contract.d.ts +33 -0
- package/lib/types/rpc-contract.d.ts.map +1 -0
- package/lib/types/rpc-vocabulary.d.ts +7 -0
- package/lib/types/rpc-vocabulary.d.ts.map +1 -0
- package/lib/types/rpc.d.ts +11 -0
- package/lib/types/rpc.d.ts.map +1 -0
- package/lib/types/safe-text.d.ts +3 -0
- package/lib/types/safe-text.d.ts.map +1 -0
- package/lib/types/search.d.ts +42 -0
- package/lib/types/search.d.ts.map +1 -0
- package/lib/types/status.d.ts +62 -0
- package/lib/types/status.d.ts.map +1 -0
- package/lib/types/video.d.ts +42 -0
- package/lib/types/video.d.ts.map +1 -0
- package/lib/types/wire-identity.d.ts +49 -0
- package/lib/types/wire-identity.d.ts.map +1 -0
- package/lib/video.js +320 -0
- package/lib/wire-identity.js +162 -0
- package/package.json +189 -0
- package/scripts/live-gates.mjs +4 -0
|
@@ -0,0 +1,1896 @@
|
|
|
1
|
+
import { t as isBoundedSafeText } from "./safe-text-AlEyN8q_.js";
|
|
2
|
+
import { ProjectDiscoveryError, createProjectDiscovery, normalizeProjectId } from "./project-context.js";
|
|
3
|
+
import { a as createStatusView, i as LLM_FAMILY_IDS, n as CAPABILITY_GATE_OUTCOMES, r as CAPABILITY_ROW_IDS } from "./status-D0-em3Ru.js";
|
|
4
|
+
import { createQuotaService } from "./quota.js";
|
|
5
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
6
|
+
import { chmod, lstat, mkdir, open, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { ANTIGRAVITY_CLIENT_ID, ANTIGRAVITY_CLIENT_SECRET, ANTIGRAVITY_REDIRECT_URI, ANTIGRAVITY_SCOPES } from "@cortexkit/antigravity-auth-core";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
const AUTH_STORE_LOCK_NAME = ".auth.lock";
|
|
11
|
+
const AUTH_STORE_LOCK_TIMEOUT_MS = 1e4;
|
|
12
|
+
const AUTH_STORE_LOCK_STALE_MS = 3e4;
|
|
13
|
+
const AUTH_STORE_LOCK_RETRY_MS = 10;
|
|
14
|
+
var AuthStoreError = class extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "AuthStoreError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** Resolve the plugin-owned default path without reading it. */
|
|
23
|
+
function defaultAuthStorePath(env = process.env, home = env.HOME, platform = process.platform) {
|
|
24
|
+
if (platform === "win32") {
|
|
25
|
+
const windowsDataHome = env.LOCALAPPDATA ?? env.APPDATA;
|
|
26
|
+
const base = typeof windowsDataHome === "string" && windowsDataHome.length > 0 ? windowsDataHome : env.USERPROFILE ?? home ?? "";
|
|
27
|
+
return join(base, "dsh-antigravity-auth", "auth.json");
|
|
28
|
+
}
|
|
29
|
+
const dataHome = env.XDG_DATA_HOME;
|
|
30
|
+
const base = typeof dataHome === "string" && dataHome.length > 0 ? dataHome : join(home ?? "", ".local", "share");
|
|
31
|
+
return join(base, "dsh-antigravity-auth", "auth.json");
|
|
32
|
+
}
|
|
33
|
+
/** Create one store whose public API never exposes an access token field. */
|
|
34
|
+
function createAuthStore(path, options = {}) {
|
|
35
|
+
const now = options.now ?? (() => Date.now());
|
|
36
|
+
const enqueue = createMutationQueue();
|
|
37
|
+
return {
|
|
38
|
+
read: () => readAuthRecord(path),
|
|
39
|
+
commit: (draft) => enqueue(() => withStoreLock(path, async () => {
|
|
40
|
+
const record = makeRecord(draft, ((await readAuthRecord(path))?.revision ?? 0) + 1, now());
|
|
41
|
+
await writeAuthRecord(path, record);
|
|
42
|
+
return record;
|
|
43
|
+
})),
|
|
44
|
+
compareAndCommit: (expectedRevision, draft, expectedLineage) => enqueue(() => withStoreLock(path, async () => {
|
|
45
|
+
const current = await readAuthRecord(path);
|
|
46
|
+
if ((current?.revision ?? 0) !== expectedRevision) return void 0;
|
|
47
|
+
const lineage = expectedLineage ?? draft.lineage;
|
|
48
|
+
if (lineage === void 0 ? current?.lineage !== void 0 : current?.lineage !== lineage) return void 0;
|
|
49
|
+
const record = makeRecord(draft, expectedRevision + 1, now());
|
|
50
|
+
await writeAuthRecord(path, record);
|
|
51
|
+
return record;
|
|
52
|
+
})),
|
|
53
|
+
clearIfCurrent: (expectedRevision, expectedLineage) => enqueue(() => withStoreLock(path, async () => {
|
|
54
|
+
const current = await readAuthRecord(path);
|
|
55
|
+
if ((current?.revision ?? 0) !== expectedRevision) return false;
|
|
56
|
+
if (expectedLineage === void 0 ? current?.lineage !== void 0 : current?.lineage !== expectedLineage) return false;
|
|
57
|
+
try {
|
|
58
|
+
await unlink(path);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!isNotFound(error)) throw storeIoError();
|
|
61
|
+
}
|
|
62
|
+
await syncDirectory(dirname(path));
|
|
63
|
+
return true;
|
|
64
|
+
})),
|
|
65
|
+
clear: () => enqueue(() => withStoreLock(path, async () => {
|
|
66
|
+
try {
|
|
67
|
+
await unlink(path);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!isNotFound(error)) throw storeIoError();
|
|
70
|
+
}
|
|
71
|
+
await syncDirectory(dirname(path));
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function createMutationQueue() {
|
|
76
|
+
let mutation = Promise.resolve();
|
|
77
|
+
return function enqueue(operation) {
|
|
78
|
+
const next = mutation.then(operation, operation);
|
|
79
|
+
mutation = next.then(() => {}, () => {});
|
|
80
|
+
return next;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function withStoreLock(path, operation) {
|
|
84
|
+
const parent = dirname(path);
|
|
85
|
+
await prepareParent(parent);
|
|
86
|
+
const lockPath = join(parent, AUTH_STORE_LOCK_NAME);
|
|
87
|
+
const deadline = Date.now() + AUTH_STORE_LOCK_TIMEOUT_MS;
|
|
88
|
+
while (true) try {
|
|
89
|
+
const handle = await open(lockPath, "wx", 384);
|
|
90
|
+
try {
|
|
91
|
+
await handle.writeFile(`${process.pid}\n`, "utf8");
|
|
92
|
+
await handle.sync();
|
|
93
|
+
return await operation();
|
|
94
|
+
} finally {
|
|
95
|
+
await handle.close().catch(() => {});
|
|
96
|
+
await unlink(lockPath).catch(() => {});
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof AuthStoreError) throw error;
|
|
100
|
+
if (!isAlreadyExists(error)) throw storeIoError();
|
|
101
|
+
await removeStaleLock(lockPath);
|
|
102
|
+
if (Date.now() >= deadline) throw conflictError();
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, AUTH_STORE_LOCK_RETRY_MS));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function prepareParent(parent) {
|
|
107
|
+
try {
|
|
108
|
+
const parentInfo = await lstat(parent);
|
|
109
|
+
if (parentInfo.isSymbolicLink() || !parentInfo.isDirectory()) throw unsafePermissionsError();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (!isNotFound(error)) throw error;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await mkdir(parent, {
|
|
115
|
+
recursive: true,
|
|
116
|
+
mode: 448
|
|
117
|
+
});
|
|
118
|
+
await chmod(parent, 448);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof AuthStoreError) throw error;
|
|
121
|
+
throw storeIoError();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function syncDirectory(path) {
|
|
125
|
+
if (process.platform === "win32") return;
|
|
126
|
+
const handle = await open(path, "r");
|
|
127
|
+
try {
|
|
128
|
+
await handle.sync();
|
|
129
|
+
} finally {
|
|
130
|
+
await handle.close().catch(() => {});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function removeStaleLock(path) {
|
|
134
|
+
try {
|
|
135
|
+
const info = await lstat(path);
|
|
136
|
+
if (Date.now() - info.mtimeMs > AUTH_STORE_LOCK_STALE_MS) await unlink(path).catch(() => {});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!isNotFound(error)) return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function readAuthRecord(path) {
|
|
142
|
+
let fileInfo;
|
|
143
|
+
try {
|
|
144
|
+
fileInfo = await lstat(path);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (isNotFound(error)) return void 0;
|
|
147
|
+
throw storeIoError();
|
|
148
|
+
}
|
|
149
|
+
if (fileInfo.isSymbolicLink() || !fileInfo.isFile()) throw unsafePermissionsError();
|
|
150
|
+
await assertOwnerOnly(path);
|
|
151
|
+
let text;
|
|
152
|
+
try {
|
|
153
|
+
text = await readFile(path, "utf8");
|
|
154
|
+
} catch {
|
|
155
|
+
throw storeIoError();
|
|
156
|
+
}
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(text);
|
|
160
|
+
} catch {
|
|
161
|
+
throw corruptError();
|
|
162
|
+
}
|
|
163
|
+
return parseRecord(parsed);
|
|
164
|
+
}
|
|
165
|
+
async function writeAuthRecord(path, record) {
|
|
166
|
+
const validated = parseRecord(record);
|
|
167
|
+
const parent = dirname(path);
|
|
168
|
+
try {
|
|
169
|
+
await prepareParent(parent);
|
|
170
|
+
const temporary = join(parent, `.${randomUUID()}.tmp`);
|
|
171
|
+
try {
|
|
172
|
+
const handle = await open(temporary, "wx", 384);
|
|
173
|
+
try {
|
|
174
|
+
await handle.writeFile(`${JSON.stringify(validated)}\n`, "utf8");
|
|
175
|
+
await handle.sync();
|
|
176
|
+
} finally {
|
|
177
|
+
await handle.close().catch(() => {});
|
|
178
|
+
}
|
|
179
|
+
await chmod(temporary, 384);
|
|
180
|
+
await rename(temporary, path);
|
|
181
|
+
await chmod(path, 384);
|
|
182
|
+
await syncDirectory(parent);
|
|
183
|
+
} catch {
|
|
184
|
+
await unlink(temporary).catch(() => {});
|
|
185
|
+
throw storeIoError();
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof AuthStoreError) throw error;
|
|
189
|
+
throw storeIoError();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function makeRecord(draft, revision, now) {
|
|
193
|
+
if (!isRecord$3(draft) || !isBoundedSafeText(draft.refreshToken, 4096) || !isBoundedSafeText(draft.projectId, 4096) || draft.lineage !== void 0 && !isBoundedSafeText(draft.lineage, 4096) || !Number.isSafeInteger(revision) || revision < 1 || !Number.isFinite(now)) throw corruptError();
|
|
194
|
+
return {
|
|
195
|
+
version: 1,
|
|
196
|
+
refreshToken: draft.refreshToken,
|
|
197
|
+
projectId: draft.projectId,
|
|
198
|
+
revision,
|
|
199
|
+
updatedAt: new Date(now).toISOString(),
|
|
200
|
+
lineage: draft.lineage ?? randomUUID(),
|
|
201
|
+
...draft.email === void 0 ? {} : { email: validateEmail(draft.email) }
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function parseRecord(value) {
|
|
205
|
+
if (!isRecord$3(value)) throw corruptError();
|
|
206
|
+
if (value.version !== 1) throw unsupportedVersionError();
|
|
207
|
+
const keys = Object.keys(value).sort();
|
|
208
|
+
const required = [
|
|
209
|
+
"projectId",
|
|
210
|
+
"refreshToken",
|
|
211
|
+
"revision",
|
|
212
|
+
"updatedAt",
|
|
213
|
+
"version"
|
|
214
|
+
];
|
|
215
|
+
const withEmail = [...required, "email"].sort();
|
|
216
|
+
const withLineage = [...required, "lineage"].sort();
|
|
217
|
+
const withEmailAndLineage = [
|
|
218
|
+
...required,
|
|
219
|
+
"email",
|
|
220
|
+
"lineage"
|
|
221
|
+
].sort();
|
|
222
|
+
const expected = keys.length === required.length ? required : keys.includes("lineage") ? keys.includes("email") ? withEmailAndLineage : withLineage : withEmail;
|
|
223
|
+
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw corruptError();
|
|
224
|
+
const refreshToken = value.refreshToken;
|
|
225
|
+
const projectId = value.projectId;
|
|
226
|
+
const revision = value.revision;
|
|
227
|
+
const updatedAt = value.updatedAt;
|
|
228
|
+
const lineage = value.lineage;
|
|
229
|
+
if (!isBoundedSafeText(refreshToken, 4096) || !isBoundedSafeText(projectId, 4096) || typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1 || typeof updatedAt !== "string" || !Number.isFinite(Date.parse(updatedAt)) || lineage !== void 0 && !isBoundedSafeText(lineage, 4096)) throw corruptError();
|
|
230
|
+
const email = value.email;
|
|
231
|
+
if (email !== void 0 && !isBoundedSafeText(email, 4096)) throw corruptError();
|
|
232
|
+
return {
|
|
233
|
+
version: 1,
|
|
234
|
+
refreshToken,
|
|
235
|
+
projectId,
|
|
236
|
+
revision,
|
|
237
|
+
updatedAt,
|
|
238
|
+
...email === void 0 ? {} : { email },
|
|
239
|
+
...lineage === void 0 ? {} : { lineage }
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function assertOwnerOnly(path) {
|
|
243
|
+
try {
|
|
244
|
+
const file = await lstat(path);
|
|
245
|
+
const parent = await lstat(dirname(path));
|
|
246
|
+
if (file.isSymbolicLink() || parent.isSymbolicLink() || !parent.isDirectory() || (file.mode & 63) !== 0 || (parent.mode & 63) !== 0) throw unsafePermissionsError();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof AuthStoreError) throw error;
|
|
249
|
+
throw storeIoError();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function validateEmail(value) {
|
|
253
|
+
if (!isBoundedSafeText(value, 4096) || !value.includes("@")) throw corruptError();
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
function corruptError() {
|
|
257
|
+
return new AuthStoreError("AUTH_STORE_CORRUPT", "The Antigravity auth store is not valid");
|
|
258
|
+
}
|
|
259
|
+
function unsupportedVersionError() {
|
|
260
|
+
return new AuthStoreError("AUTH_STORE_UNSUPPORTED_VERSION", "The Antigravity auth store version is unsupported");
|
|
261
|
+
}
|
|
262
|
+
function unsafePermissionsError() {
|
|
263
|
+
return new AuthStoreError("AUTH_STORE_UNSAFE_PERMISSIONS", "The Antigravity auth store permissions are unsafe");
|
|
264
|
+
}
|
|
265
|
+
function storeIoError() {
|
|
266
|
+
return new AuthStoreError("AUTH_STORE_IO", "The Antigravity auth store could not be accessed");
|
|
267
|
+
}
|
|
268
|
+
function conflictError() {
|
|
269
|
+
return new AuthStoreError("AUTH_STORE_CONFLICT", "The Antigravity auth store is busy");
|
|
270
|
+
}
|
|
271
|
+
function isAlreadyExists(error) {
|
|
272
|
+
return error?.code === "EEXIST";
|
|
273
|
+
}
|
|
274
|
+
function isNotFound(error) {
|
|
275
|
+
return error?.code === "ENOENT";
|
|
276
|
+
}
|
|
277
|
+
function isRecord$3(value) {
|
|
278
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/credential-coordinator.ts
|
|
282
|
+
const ANTIGRAVITY_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
283
|
+
const ANTIGRAVITY_REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke";
|
|
284
|
+
const DEFAULT_REFRESH_LEAD_MS = 3e4;
|
|
285
|
+
const DEFAULT_OPERATION_TIMEOUT_MS = 1e4;
|
|
286
|
+
const MAX_REFRESH_ATTEMPTS = 2;
|
|
287
|
+
const MAX_RESPONSE_BYTES = 65536;
|
|
288
|
+
const MAX_EXPIRES_IN_SECONDS = 31536e3;
|
|
289
|
+
var CredentialOperationError = class extends Error {
|
|
290
|
+
code;
|
|
291
|
+
constructor(code, message = credentialErrorMessage(code) ?? "The Antigravity credential operation failed") {
|
|
292
|
+
super(message);
|
|
293
|
+
this.name = "CredentialOperationError";
|
|
294
|
+
this.code = code;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
function createCredentialCoordinator(options) {
|
|
298
|
+
const now = options.now ?? (() => Date.now());
|
|
299
|
+
const refreshAccessToken = options.refreshToken ?? createGoogleRefreshTransport(options.fetchImpl, now);
|
|
300
|
+
const revokeGrant = options.revokeGrant ?? createGoogleRevokeTransport(options.fetchImpl);
|
|
301
|
+
const refreshLeadMs = Math.max(0, options.refreshLeadMs ?? DEFAULT_REFRESH_LEAD_MS);
|
|
302
|
+
const operationTimeoutMs = Math.max(1, options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS);
|
|
303
|
+
let cached;
|
|
304
|
+
let observedRevision = 0;
|
|
305
|
+
let observedLineage;
|
|
306
|
+
let state = "logged-out";
|
|
307
|
+
let errorCode;
|
|
308
|
+
let lastRefreshAt;
|
|
309
|
+
let refreshFlight;
|
|
310
|
+
let revokeFlight;
|
|
311
|
+
let revokeStatus = { state: "idle" };
|
|
312
|
+
let generation = 0;
|
|
313
|
+
let disposed = false;
|
|
314
|
+
const lifecycleAbort = new AbortController();
|
|
315
|
+
const operations = /* @__PURE__ */ new Set();
|
|
316
|
+
const coordinator = {
|
|
317
|
+
credential: async (signal, credentialOptions) => {
|
|
318
|
+
ensureNotDisposed();
|
|
319
|
+
if (signal?.aborted === true) throw new CredentialOperationError("cancelled");
|
|
320
|
+
const record = await options.store.read();
|
|
321
|
+
if (record === void 0) {
|
|
322
|
+
clearObservedCredential();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
observe(record);
|
|
326
|
+
if (state === "re-login-required") return void 0;
|
|
327
|
+
if (credentialOptions?.forceRefresh !== true && cached !== void 0 && sameRecord(cached, record) && isFresh(cached.value, now(), refreshLeadMs)) {
|
|
328
|
+
state = "logged-in";
|
|
329
|
+
errorCode = void 0;
|
|
330
|
+
return await waitForCaller(Promise.resolve(cached.value), signal);
|
|
331
|
+
}
|
|
332
|
+
if (refreshFlight === void 0) {
|
|
333
|
+
const flight = refreshCredential(generation);
|
|
334
|
+
refreshFlight = flight;
|
|
335
|
+
flight.then(() => clearRefreshFlight(flight), () => clearRefreshFlight(flight));
|
|
336
|
+
}
|
|
337
|
+
return await waitForCaller(refreshFlight, signal);
|
|
338
|
+
},
|
|
339
|
+
replaceFromLogin: (credential, record) => {
|
|
340
|
+
if (disposed) return;
|
|
341
|
+
generation += 1;
|
|
342
|
+
abortOperations();
|
|
343
|
+
cached = {
|
|
344
|
+
value: { ...credential },
|
|
345
|
+
revision: record.revision,
|
|
346
|
+
...record.lineage === void 0 ? {} : { lineage: record.lineage }
|
|
347
|
+
};
|
|
348
|
+
observedRevision = record.revision;
|
|
349
|
+
observedLineage = record.lineage;
|
|
350
|
+
state = "logged-in";
|
|
351
|
+
errorCode = void 0;
|
|
352
|
+
lastRefreshAt = void 0;
|
|
353
|
+
revokeStatus = { state: "idle" };
|
|
354
|
+
refreshFlight = void 0;
|
|
355
|
+
revokeFlight = void 0;
|
|
356
|
+
},
|
|
357
|
+
status: async () => {
|
|
358
|
+
ensureNotDisposed();
|
|
359
|
+
const record = await options.store.read();
|
|
360
|
+
if (record === void 0) clearObservedCredential();
|
|
361
|
+
else observe(record);
|
|
362
|
+
return makeCredentialStatus(record);
|
|
363
|
+
},
|
|
364
|
+
revokeStatus: () => ({ ...revokeStatus }),
|
|
365
|
+
logout: async () => {
|
|
366
|
+
ensureNotDisposed();
|
|
367
|
+
generation += 1;
|
|
368
|
+
const logoutGeneration = generation;
|
|
369
|
+
abortOperations();
|
|
370
|
+
refreshFlight = void 0;
|
|
371
|
+
revokeFlight = void 0;
|
|
372
|
+
cached = void 0;
|
|
373
|
+
observedRevision = 0;
|
|
374
|
+
observedLineage = void 0;
|
|
375
|
+
const record = await options.store.read();
|
|
376
|
+
if (!isActive(logoutGeneration)) return { state: "logged-out" };
|
|
377
|
+
let cleared = true;
|
|
378
|
+
if (record !== void 0) cleared = await options.store.clearIfCurrent(record.revision, record.lineage);
|
|
379
|
+
if (!isActive(logoutGeneration)) return { state: "logged-out" };
|
|
380
|
+
if (!cleared) {
|
|
381
|
+
const latest = await options.store.read();
|
|
382
|
+
if (!isActive(logoutGeneration)) return { state: "logged-out" };
|
|
383
|
+
if (latest === void 0) {
|
|
384
|
+
clearObservedCredential();
|
|
385
|
+
revokeStatus = { state: "logged-out" };
|
|
386
|
+
} else observe(latest);
|
|
387
|
+
return { state: "logged-out" };
|
|
388
|
+
}
|
|
389
|
+
state = "logged-out";
|
|
390
|
+
errorCode = void 0;
|
|
391
|
+
lastRefreshAt = void 0;
|
|
392
|
+
revokeStatus = { state: "logged-out" };
|
|
393
|
+
return { state: "logged-out" };
|
|
394
|
+
},
|
|
395
|
+
revoke: async (confirmed, signal) => {
|
|
396
|
+
ensureNotDisposed();
|
|
397
|
+
if (!confirmed) {
|
|
398
|
+
revokeStatus = { state: "confirmation-required" };
|
|
399
|
+
return { state: "confirmation-required" };
|
|
400
|
+
}
|
|
401
|
+
if (signal?.aborted === true) throw new CredentialOperationError("cancelled");
|
|
402
|
+
if (revokeFlight !== void 0) return await waitForCaller(revokeFlight, signal);
|
|
403
|
+
generation += 1;
|
|
404
|
+
abortOperations();
|
|
405
|
+
refreshFlight = void 0;
|
|
406
|
+
const revokeGeneration = generation;
|
|
407
|
+
const record = await options.store.read();
|
|
408
|
+
if (!isActive(revokeGeneration)) return { state: "superseded" };
|
|
409
|
+
if (record === void 0) {
|
|
410
|
+
clearObservedCredential();
|
|
411
|
+
revokeStatus = { state: "logged-out" };
|
|
412
|
+
return { state: "logged-out" };
|
|
413
|
+
}
|
|
414
|
+
revokeStatus = { state: "pending" };
|
|
415
|
+
const flight = revokeCredential(record, revokeGeneration);
|
|
416
|
+
revokeFlight = flight;
|
|
417
|
+
flight.then(() => clearRevokeFlight(flight), () => clearRevokeFlight(flight));
|
|
418
|
+
return await waitForCaller(flight, signal);
|
|
419
|
+
},
|
|
420
|
+
dispose: async () => {
|
|
421
|
+
if (disposed) return;
|
|
422
|
+
disposed = true;
|
|
423
|
+
generation += 1;
|
|
424
|
+
lifecycleAbort.abort(new CredentialOperationError("cancelled", "The Antigravity credential service was disposed"));
|
|
425
|
+
abortOperations();
|
|
426
|
+
refreshFlight = void 0;
|
|
427
|
+
revokeFlight = void 0;
|
|
428
|
+
cached = void 0;
|
|
429
|
+
operations.clear();
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
function ensureNotDisposed() {
|
|
433
|
+
if (disposed) throw new CredentialOperationError("cancelled", "The Antigravity credential service is unavailable");
|
|
434
|
+
}
|
|
435
|
+
function beginOperation() {
|
|
436
|
+
const controller = new AbortController();
|
|
437
|
+
operations.add(controller);
|
|
438
|
+
if (lifecycleAbort.signal.aborted) controller.abort(lifecycleAbort.signal.reason);
|
|
439
|
+
return controller;
|
|
440
|
+
}
|
|
441
|
+
function endOperation(controller) {
|
|
442
|
+
operations.delete(controller);
|
|
443
|
+
}
|
|
444
|
+
function abortOperations() {
|
|
445
|
+
for (const controller of operations) controller.abort(new CredentialOperationError("cancelled"));
|
|
446
|
+
operations.clear();
|
|
447
|
+
}
|
|
448
|
+
function clearRefreshFlight(flight) {
|
|
449
|
+
if (refreshFlight === flight) refreshFlight = void 0;
|
|
450
|
+
}
|
|
451
|
+
function clearRevokeFlight(flight) {
|
|
452
|
+
if (revokeFlight === flight) revokeFlight = void 0;
|
|
453
|
+
}
|
|
454
|
+
function clearObservedCredential() {
|
|
455
|
+
cached = void 0;
|
|
456
|
+
observedRevision = 0;
|
|
457
|
+
observedLineage = void 0;
|
|
458
|
+
if (!disposed) {
|
|
459
|
+
state = "logged-out";
|
|
460
|
+
errorCode = void 0;
|
|
461
|
+
lastRefreshAt = void 0;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function observe(record) {
|
|
465
|
+
const lineageChanged = observedRevision !== 0 && observedLineage !== record.lineage;
|
|
466
|
+
if (observedRevision !== 0 && (observedRevision !== record.revision || lineageChanged)) {
|
|
467
|
+
cached = void 0;
|
|
468
|
+
state = "logged-in";
|
|
469
|
+
errorCode = void 0;
|
|
470
|
+
lastRefreshAt = void 0;
|
|
471
|
+
}
|
|
472
|
+
observedRevision = record.revision;
|
|
473
|
+
observedLineage = record.lineage;
|
|
474
|
+
if (state === "logged-out") state = "logged-in";
|
|
475
|
+
}
|
|
476
|
+
function isFresh(credential, timestamp, lead) {
|
|
477
|
+
return Number.isFinite(credential.expiresAt) && credential.expiresAt - timestamp > lead;
|
|
478
|
+
}
|
|
479
|
+
function sameRecord(value, record) {
|
|
480
|
+
return value.revision === record.revision && value.lineage === record.lineage;
|
|
481
|
+
}
|
|
482
|
+
function makeCredentialStatus(record) {
|
|
483
|
+
const configured = record !== void 0;
|
|
484
|
+
const expiresAt = cached === void 0 ? void 0 : new Date(cached.value.expiresAt).toISOString();
|
|
485
|
+
return {
|
|
486
|
+
state: configured ? state : "logged-out",
|
|
487
|
+
configured,
|
|
488
|
+
...expiresAt === void 0 ? {} : { expiresAt },
|
|
489
|
+
...lastRefreshAt === void 0 ? {} : { lastRefreshAt: new Date(lastRefreshAt).toISOString() },
|
|
490
|
+
...errorCode === void 0 ? {} : { errorCode }
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function refreshCredential(startGeneration) {
|
|
494
|
+
if (disposed || startGeneration !== generation) return void 0;
|
|
495
|
+
let record = await options.store.read();
|
|
496
|
+
if (!isActive(startGeneration)) return void 0;
|
|
497
|
+
if (record === void 0) {
|
|
498
|
+
clearObservedCredential();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
observe(record);
|
|
502
|
+
if (state === "re-login-required") return void 0;
|
|
503
|
+
for (let attempt = 0; attempt < MAX_REFRESH_ATTEMPTS; attempt += 1) {
|
|
504
|
+
if (!isActive(startGeneration)) return void 0;
|
|
505
|
+
state = "refreshing";
|
|
506
|
+
errorCode = void 0;
|
|
507
|
+
const operation = beginOperation();
|
|
508
|
+
let result;
|
|
509
|
+
try {
|
|
510
|
+
result = await runBounded((signal) => refreshAccessToken({
|
|
511
|
+
refreshToken: record.refreshToken,
|
|
512
|
+
signal
|
|
513
|
+
}), operation, operationTimeoutMs);
|
|
514
|
+
validateRefreshResult(result);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
endOperation(operation);
|
|
517
|
+
if (!isActive(startGeneration) || isCancelled(error)) return void 0;
|
|
518
|
+
setRefreshFailure(error);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
endOperation(operation);
|
|
522
|
+
if (!isActive(startGeneration)) return void 0;
|
|
523
|
+
const current = await options.store.read();
|
|
524
|
+
if (!isActive(startGeneration)) return void 0;
|
|
525
|
+
const responseRefreshToken = result.refreshToken ?? record.refreshToken;
|
|
526
|
+
if (current === void 0) {
|
|
527
|
+
clearObservedCredential();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (sameLineage(current, record) && current.revision !== record.revision) {
|
|
531
|
+
if (current.refreshToken === responseRefreshToken) return adoptRefreshedCredential(result, current, startGeneration);
|
|
532
|
+
record = current;
|
|
533
|
+
observe(record);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (!sameLineage(current, record)) {
|
|
537
|
+
record = current;
|
|
538
|
+
observe(record);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const committed = await options.store.compareAndCommit(record.revision, {
|
|
542
|
+
refreshToken: responseRefreshToken,
|
|
543
|
+
projectId: record.projectId,
|
|
544
|
+
...record.email === void 0 ? {} : { email: record.email },
|
|
545
|
+
...record.lineage === void 0 ? {} : { lineage: record.lineage }
|
|
546
|
+
}, record.lineage);
|
|
547
|
+
if (committed !== void 0) return adoptRefreshedCredential(result, committed, startGeneration);
|
|
548
|
+
const latest = await options.store.read();
|
|
549
|
+
if (!isActive(startGeneration)) return void 0;
|
|
550
|
+
if (latest === void 0) {
|
|
551
|
+
clearObservedCredential();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (latest.refreshToken === responseRefreshToken && sameLineage(latest, record)) return adoptRefreshedCredential(result, latest, startGeneration);
|
|
555
|
+
record = latest;
|
|
556
|
+
observe(record);
|
|
557
|
+
}
|
|
558
|
+
if (isActive(startGeneration)) {
|
|
559
|
+
state = "refresh-failed";
|
|
560
|
+
errorCode = "conflict";
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function adoptRefreshedCredential(result, record, startGeneration) {
|
|
564
|
+
if (!isActive(startGeneration)) return void 0;
|
|
565
|
+
const credential = {
|
|
566
|
+
accessToken: result.accessToken,
|
|
567
|
+
refreshToken: result.refreshToken ?? record.refreshToken,
|
|
568
|
+
expiresAt: result.expiresAt,
|
|
569
|
+
projectId: record.projectId
|
|
570
|
+
};
|
|
571
|
+
cached = {
|
|
572
|
+
value: credential,
|
|
573
|
+
revision: record.revision,
|
|
574
|
+
...record.lineage === void 0 ? {} : { lineage: record.lineage }
|
|
575
|
+
};
|
|
576
|
+
observedRevision = record.revision;
|
|
577
|
+
observedLineage = record.lineage;
|
|
578
|
+
state = "logged-in";
|
|
579
|
+
errorCode = void 0;
|
|
580
|
+
lastRefreshAt = now();
|
|
581
|
+
return { ...credential };
|
|
582
|
+
}
|
|
583
|
+
async function revokeCredential(record, revokeGeneration) {
|
|
584
|
+
const operation = beginOperation();
|
|
585
|
+
try {
|
|
586
|
+
await runBounded((signal) => revokeGrant({
|
|
587
|
+
token: record.refreshToken,
|
|
588
|
+
signal
|
|
589
|
+
}), operation, operationTimeoutMs);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
endOperation(operation);
|
|
592
|
+
if (!isActive(revokeGeneration) || isCancelled(error)) return { state: "superseded" };
|
|
593
|
+
const code = toRevokeErrorCode(error);
|
|
594
|
+
revokeStatus = {
|
|
595
|
+
state: "failed",
|
|
596
|
+
errorCode: code
|
|
597
|
+
};
|
|
598
|
+
return {
|
|
599
|
+
state: "failed",
|
|
600
|
+
errorCode: code
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
endOperation(operation);
|
|
604
|
+
if (!isActive(revokeGeneration)) return { state: "superseded" };
|
|
605
|
+
let cleared;
|
|
606
|
+
try {
|
|
607
|
+
cleared = await options.store.clearIfCurrent(record.revision, record.lineage);
|
|
608
|
+
} catch {
|
|
609
|
+
if (!isActive(revokeGeneration)) return { state: "superseded" };
|
|
610
|
+
revokeStatus = {
|
|
611
|
+
state: "failed",
|
|
612
|
+
errorCode: "storage"
|
|
613
|
+
};
|
|
614
|
+
return {
|
|
615
|
+
state: "failed",
|
|
616
|
+
errorCode: "storage"
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (!isActive(revokeGeneration)) return { state: "superseded" };
|
|
620
|
+
if (!cleared) {
|
|
621
|
+
revokeStatus = { state: "superseded" };
|
|
622
|
+
return { state: "superseded" };
|
|
623
|
+
}
|
|
624
|
+
cached = void 0;
|
|
625
|
+
observedRevision = 0;
|
|
626
|
+
observedLineage = void 0;
|
|
627
|
+
state = "logged-out";
|
|
628
|
+
errorCode = void 0;
|
|
629
|
+
lastRefreshAt = void 0;
|
|
630
|
+
revokeStatus = { state: "revoked" };
|
|
631
|
+
return { state: "revoked" };
|
|
632
|
+
}
|
|
633
|
+
function isActive(expectedGeneration) {
|
|
634
|
+
return !disposed && expectedGeneration === generation;
|
|
635
|
+
}
|
|
636
|
+
function setRefreshFailure(error) {
|
|
637
|
+
const code = error instanceof CredentialOperationError ? error.code : "network";
|
|
638
|
+
state = code === "invalid-grant" ? "re-login-required" : "refresh-failed";
|
|
639
|
+
errorCode = code;
|
|
640
|
+
cached = void 0;
|
|
641
|
+
}
|
|
642
|
+
function sameLineage(left, right) {
|
|
643
|
+
if (left.lineage !== void 0 || right.lineage !== void 0) return left.lineage === right.lineage;
|
|
644
|
+
return left.revision === right.revision;
|
|
645
|
+
}
|
|
646
|
+
function toRevokeErrorCode(error) {
|
|
647
|
+
const code = error instanceof CredentialOperationError ? error.code : "network";
|
|
648
|
+
if (code === "invalid-grant" || code === "conflict" || code === "cancelled") return "http-error";
|
|
649
|
+
return code;
|
|
650
|
+
}
|
|
651
|
+
return coordinator;
|
|
652
|
+
}
|
|
653
|
+
function createGoogleRefreshTransport(fetchImpl = globalThis.fetch, now = () => Date.now()) {
|
|
654
|
+
return async ({ refreshToken, signal }) => {
|
|
655
|
+
let response;
|
|
656
|
+
try {
|
|
657
|
+
response = await fetchImpl(ANTIGRAVITY_TOKEN_ENDPOINT, {
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers: {
|
|
660
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
661
|
+
Accept: "application/json"
|
|
662
|
+
},
|
|
663
|
+
body: new URLSearchParams({
|
|
664
|
+
client_id: ANTIGRAVITY_CLIENT_ID,
|
|
665
|
+
client_secret: ANTIGRAVITY_CLIENT_SECRET,
|
|
666
|
+
grant_type: "refresh_token",
|
|
667
|
+
refresh_token: refreshToken
|
|
668
|
+
}),
|
|
669
|
+
signal
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (isAbortError(error)) throw new CredentialOperationError("cancelled");
|
|
673
|
+
throw new CredentialOperationError("network");
|
|
674
|
+
}
|
|
675
|
+
if (!response.ok && (response.status === 429 || response.status >= 500)) throw responseError(response.status);
|
|
676
|
+
const body = await readJsonBody(response);
|
|
677
|
+
if (!response.ok) throw responseError(response.status, body);
|
|
678
|
+
if (!isRecord$2(body) || typeof body.access_token !== "string" || !isBoundedSafeText(body.access_token, 4096)) throw new CredentialOperationError("invalid-response");
|
|
679
|
+
const expiresIn = body.expires_in;
|
|
680
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > MAX_EXPIRES_IN_SECONDS) throw new CredentialOperationError("invalid-response");
|
|
681
|
+
const nextRefreshToken = body.refresh_token;
|
|
682
|
+
if (nextRefreshToken !== void 0 && (typeof nextRefreshToken !== "string" || !isBoundedSafeText(nextRefreshToken, 4096))) throw new CredentialOperationError("invalid-response");
|
|
683
|
+
return {
|
|
684
|
+
accessToken: body.access_token,
|
|
685
|
+
expiresAt: now() + expiresIn * 1e3,
|
|
686
|
+
...nextRefreshToken === void 0 ? {} : { refreshToken: nextRefreshToken }
|
|
687
|
+
};
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function createGoogleRevokeTransport(fetchImpl = globalThis.fetch) {
|
|
691
|
+
return async ({ token, signal }) => {
|
|
692
|
+
let response;
|
|
693
|
+
try {
|
|
694
|
+
response = await fetchImpl(ANTIGRAVITY_REVOKE_ENDPOINT, {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: {
|
|
697
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
698
|
+
Accept: "*/*"
|
|
699
|
+
},
|
|
700
|
+
body: new URLSearchParams({ token }),
|
|
701
|
+
signal
|
|
702
|
+
});
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (isAbortError(error)) throw new CredentialOperationError("cancelled");
|
|
705
|
+
throw new CredentialOperationError("network");
|
|
706
|
+
}
|
|
707
|
+
if (!response.ok) throw responseError(response.status);
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async function runBounded(operation, parent, timeoutMs) {
|
|
711
|
+
const controller = new AbortController();
|
|
712
|
+
let timer;
|
|
713
|
+
let removeParentAbort;
|
|
714
|
+
const operationPromise = Promise.resolve().then(() => operation(controller.signal));
|
|
715
|
+
operationPromise.catch(() => {});
|
|
716
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
717
|
+
timer = setTimeout(() => {
|
|
718
|
+
controller.abort(new CredentialOperationError("timeout"));
|
|
719
|
+
reject(new CredentialOperationError("timeout"));
|
|
720
|
+
}, timeoutMs);
|
|
721
|
+
});
|
|
722
|
+
const abortPromise = new Promise((_, reject) => {
|
|
723
|
+
const abort = () => {
|
|
724
|
+
controller.abort(parent.signal.reason);
|
|
725
|
+
reject(new CredentialOperationError("cancelled"));
|
|
726
|
+
};
|
|
727
|
+
removeParentAbort = () => parent.signal.removeEventListener("abort", abort);
|
|
728
|
+
if (parent.signal.aborted) abort();
|
|
729
|
+
else parent.signal.addEventListener("abort", abort, { once: true });
|
|
730
|
+
});
|
|
731
|
+
try {
|
|
732
|
+
return await Promise.race([
|
|
733
|
+
operationPromise,
|
|
734
|
+
timeoutPromise,
|
|
735
|
+
abortPromise
|
|
736
|
+
]);
|
|
737
|
+
} finally {
|
|
738
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
739
|
+
removeParentAbort?.();
|
|
740
|
+
controller.abort();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async function waitForCaller(promise, signal) {
|
|
744
|
+
if (signal === void 0) return await promise;
|
|
745
|
+
if (signal.aborted) throw new CredentialOperationError("cancelled");
|
|
746
|
+
return await new Promise((resolve, reject) => {
|
|
747
|
+
let settled = false;
|
|
748
|
+
const abort = () => {
|
|
749
|
+
if (settled) return;
|
|
750
|
+
settled = true;
|
|
751
|
+
reject(new CredentialOperationError("cancelled"));
|
|
752
|
+
};
|
|
753
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
754
|
+
promise.then((value) => {
|
|
755
|
+
if (settled) return;
|
|
756
|
+
settled = true;
|
|
757
|
+
signal.removeEventListener("abort", abort);
|
|
758
|
+
resolve(value);
|
|
759
|
+
}, (error) => {
|
|
760
|
+
if (settled) return;
|
|
761
|
+
settled = true;
|
|
762
|
+
signal.removeEventListener("abort", abort);
|
|
763
|
+
reject(error);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
async function readJsonBody(response) {
|
|
768
|
+
const length = response.headers.get("content-length");
|
|
769
|
+
if (length !== null && Number.isFinite(Number(length)) && Number(length) > MAX_RESPONSE_BYTES) throw new CredentialOperationError("invalid-response");
|
|
770
|
+
let text;
|
|
771
|
+
try {
|
|
772
|
+
text = await response.text();
|
|
773
|
+
} catch {
|
|
774
|
+
throw new CredentialOperationError("invalid-response");
|
|
775
|
+
}
|
|
776
|
+
if (new TextEncoder().encode(text).byteLength > MAX_RESPONSE_BYTES) throw new CredentialOperationError("invalid-response");
|
|
777
|
+
if (text.length === 0) return {};
|
|
778
|
+
try {
|
|
779
|
+
return JSON.parse(text);
|
|
780
|
+
} catch {
|
|
781
|
+
throw new CredentialOperationError("invalid-response");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function responseError(status, body) {
|
|
785
|
+
if (status === 400 && isRecord$2(body) && body.error === "invalid_grant") return new CredentialOperationError("invalid-grant");
|
|
786
|
+
if (status === 408 || status === 504) return new CredentialOperationError("timeout");
|
|
787
|
+
if (status === 429) return new CredentialOperationError("rate-limited");
|
|
788
|
+
if (status >= 500 && status <= 599) return new CredentialOperationError("server-error");
|
|
789
|
+
return new CredentialOperationError("http-error");
|
|
790
|
+
}
|
|
791
|
+
function validateRefreshResult(value) {
|
|
792
|
+
if (!isRecord$2(value) || typeof value.accessToken !== "string" || !isBoundedSafeText(value.accessToken, 4096) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) throw new CredentialOperationError("invalid-response");
|
|
793
|
+
if (value.refreshToken !== void 0 && (typeof value.refreshToken !== "string" || !isBoundedSafeText(value.refreshToken, 4096))) throw new CredentialOperationError("invalid-response");
|
|
794
|
+
}
|
|
795
|
+
function isCancelled(error) {
|
|
796
|
+
return error instanceof CredentialOperationError && error.code === "cancelled";
|
|
797
|
+
}
|
|
798
|
+
function isAbortError(error) {
|
|
799
|
+
return error instanceof Error && error.name === "AbortError";
|
|
800
|
+
}
|
|
801
|
+
function credentialErrorMessage(code) {
|
|
802
|
+
switch (code) {
|
|
803
|
+
case "invalid-grant": return "The Antigravity grant requires login again";
|
|
804
|
+
case "timeout": return "The Antigravity authentication request timed out";
|
|
805
|
+
case "rate-limited": return "The Antigravity authentication service is rate-limited";
|
|
806
|
+
case "server-error": return "The Antigravity authentication service is unavailable";
|
|
807
|
+
case "network": return "The Antigravity authentication request failed";
|
|
808
|
+
case "invalid-response": return "The Antigravity authentication response was invalid";
|
|
809
|
+
case "conflict": return "The Antigravity credential changed while it was refreshing";
|
|
810
|
+
case "storage": return "The Antigravity credential store failed";
|
|
811
|
+
case "cancelled": return "The Antigravity authentication request was cancelled";
|
|
812
|
+
case "http-error": return "The Antigravity authentication service rejected the request";
|
|
813
|
+
default: return;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function isRecord$2(value) {
|
|
817
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/oauth-flow.ts
|
|
821
|
+
/** Host-only PKCE, loopback callback, and token-exchange coordinator. */
|
|
822
|
+
const ANTIGRAVITY_CALLBACK_PORT = 51121;
|
|
823
|
+
const ANTIGRAVITY_CALLBACK_HOSTS = Object.freeze([
|
|
824
|
+
`localhost:${String(ANTIGRAVITY_CALLBACK_PORT)}`,
|
|
825
|
+
`127.0.0.1:${String(ANTIGRAVITY_CALLBACK_PORT)}`,
|
|
826
|
+
`[::1]:${String(ANTIGRAVITY_CALLBACK_PORT)}`,
|
|
827
|
+
"localhost",
|
|
828
|
+
"127.0.0.1",
|
|
829
|
+
"[::1]"
|
|
830
|
+
]);
|
|
831
|
+
const AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
832
|
+
const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
833
|
+
const MAX_CALLBACK_VALUE_LENGTH = 4096;
|
|
834
|
+
const MAX_TOKEN_RESPONSE_BYTES = 65536;
|
|
835
|
+
const EMPTY_RESPONSE_HEADERS = Object.freeze({
|
|
836
|
+
"Cache-Control": "no-store",
|
|
837
|
+
"Content-Security-Policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
838
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
839
|
+
"X-Content-Type-Options": "nosniff"
|
|
840
|
+
});
|
|
841
|
+
var OAuthFlowError = class extends Error {
|
|
842
|
+
code;
|
|
843
|
+
constructor(code, message) {
|
|
844
|
+
super(message);
|
|
845
|
+
this.name = "OAuthFlowError";
|
|
846
|
+
this.code = code;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
/** Create one process-local, one-shot OAuth flow. */
|
|
850
|
+
function createOAuthFlow(options = {}) {
|
|
851
|
+
const clock = options.clock ?? systemClock();
|
|
852
|
+
const random = options.randomBytes ?? ((size) => randomBytes(size));
|
|
853
|
+
const listenerFactory = options.listenerFactory ?? createNodeLoopbackListenerFactory();
|
|
854
|
+
const exchangeCode = options.exchangeCode ?? createGoogleTokenExchanger(options.fetchImpl ?? fetch, clock);
|
|
855
|
+
const validateProject = options.validateProject ?? (async () => void 0);
|
|
856
|
+
const commit = options.commit ?? (async () => {});
|
|
857
|
+
const ttlMs = options.ttlMs ?? 3e5;
|
|
858
|
+
let pending;
|
|
859
|
+
let processing;
|
|
860
|
+
let generation = 0;
|
|
861
|
+
let committingGeneration;
|
|
862
|
+
let currentStatus = { phase: "idle" };
|
|
863
|
+
let disposed = false;
|
|
864
|
+
let listenerClosing = Promise.resolve();
|
|
865
|
+
const closeListener = (candidate) => {
|
|
866
|
+
const listener = candidate.listener;
|
|
867
|
+
candidate.listener = void 0;
|
|
868
|
+
if (listener === void 0) return listenerClosing;
|
|
869
|
+
listenerClosing = listenerClosing.then(() => Promise.resolve(listener.close()).catch(() => {}), () => Promise.resolve(listener.close()).catch(() => {}));
|
|
870
|
+
return listenerClosing;
|
|
871
|
+
};
|
|
872
|
+
const flow = {
|
|
873
|
+
generation: () => committingGeneration ?? generation,
|
|
874
|
+
start: async () => {
|
|
875
|
+
if (disposed) throw new OAuthFlowError("internal", "The OAuth flow is unavailable");
|
|
876
|
+
if (pending !== void 0) await cancelPending(pending, "cancelled");
|
|
877
|
+
if (processing !== void 0) {
|
|
878
|
+
if (!processing.commitStarted) processing.controller.abort(new OAuthFlowError("cancelled", "The OAuth login was cancelled"));
|
|
879
|
+
processing = void 0;
|
|
880
|
+
}
|
|
881
|
+
const verifier = encodeBase64Url(randomBytes$1(random, 32));
|
|
882
|
+
const state = encodeBase64Url(randomBytes$1(random, 32));
|
|
883
|
+
const authorizationUrl = buildAuthorizationUrl(state, verifier);
|
|
884
|
+
const expiresAt = clock.now() + ttlMs;
|
|
885
|
+
const controller = new AbortController();
|
|
886
|
+
const candidateGeneration = generation + 1;
|
|
887
|
+
generation = candidateGeneration;
|
|
888
|
+
const candidate = {
|
|
889
|
+
authorizationUrl,
|
|
890
|
+
expiresAt,
|
|
891
|
+
state,
|
|
892
|
+
verifier,
|
|
893
|
+
controller,
|
|
894
|
+
generation: candidateGeneration,
|
|
895
|
+
timeout: clock.setTimeout(() => {
|
|
896
|
+
expire(candidate);
|
|
897
|
+
}, ttlMs),
|
|
898
|
+
active: true,
|
|
899
|
+
commitStarted: false
|
|
900
|
+
};
|
|
901
|
+
pending = candidate;
|
|
902
|
+
currentStatus = {
|
|
903
|
+
phase: "pending",
|
|
904
|
+
authorizationUrl,
|
|
905
|
+
expiresAt: new Date(expiresAt).toISOString()
|
|
906
|
+
};
|
|
907
|
+
try {
|
|
908
|
+
await listenerClosing;
|
|
909
|
+
if (!candidate.active || pending !== candidate) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
910
|
+
candidate.listener = await listenerFactory.listen((request) => handleLoopbackRequest(request));
|
|
911
|
+
if (!candidate.active || pending !== candidate) {
|
|
912
|
+
await closeListener(candidate);
|
|
913
|
+
throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
914
|
+
}
|
|
915
|
+
} catch (error) {
|
|
916
|
+
clock.clearTimeout(candidate.timeout);
|
|
917
|
+
if (pending === candidate) pending = void 0;
|
|
918
|
+
candidate.active = false;
|
|
919
|
+
const safe = asListenerError(error);
|
|
920
|
+
updateStatus(candidate, {
|
|
921
|
+
phase: safe.code === "port-conflict" ? "port-conflict" : "failed",
|
|
922
|
+
errorCode: safe.code
|
|
923
|
+
});
|
|
924
|
+
throw safe;
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
started: true,
|
|
928
|
+
phase: "pending",
|
|
929
|
+
authorizationUrl,
|
|
930
|
+
expiresAt: new Date(expiresAt).toISOString()
|
|
931
|
+
};
|
|
932
|
+
},
|
|
933
|
+
status: () => currentStatus,
|
|
934
|
+
completeCallbackUrl: async (callbackUrl) => {
|
|
935
|
+
if (typeof callbackUrl !== "string" || callbackUrl.length === 0 || callbackUrl.length > MAX_CALLBACK_VALUE_LENGTH) throw new OAuthFlowError("invalid-callback-url", "The callback URL is invalid");
|
|
936
|
+
let parsed;
|
|
937
|
+
try {
|
|
938
|
+
parsed = new URL(callbackUrl);
|
|
939
|
+
} catch {
|
|
940
|
+
throw new OAuthFlowError("invalid-callback-url", "The callback URL is invalid");
|
|
941
|
+
}
|
|
942
|
+
if (parsed.protocol !== "http:" || parsed.username.length > 0 || parsed.password.length > 0 || parsed.hash.length > 0) throw new OAuthFlowError("invalid-callback-url", "The callback URL is invalid");
|
|
943
|
+
return completeCallback({
|
|
944
|
+
method: "GET",
|
|
945
|
+
host: parsed.host,
|
|
946
|
+
url: `${parsed.pathname}${parsed.search}`
|
|
947
|
+
});
|
|
948
|
+
},
|
|
949
|
+
cancel: async () => {
|
|
950
|
+
if (pending !== void 0) await cancelPending(pending, "cancelled");
|
|
951
|
+
if (processing !== void 0) {
|
|
952
|
+
const active = processing;
|
|
953
|
+
if (active.commitStarted) return currentStatus;
|
|
954
|
+
active.controller.abort(new OAuthFlowError("cancelled", "The OAuth login was cancelled"));
|
|
955
|
+
updateStatus(active, {
|
|
956
|
+
phase: "cancelled",
|
|
957
|
+
errorCode: "cancelled"
|
|
958
|
+
});
|
|
959
|
+
return currentStatus;
|
|
960
|
+
}
|
|
961
|
+
return currentStatus;
|
|
962
|
+
},
|
|
963
|
+
dispose: async () => {
|
|
964
|
+
if (disposed) return;
|
|
965
|
+
disposed = true;
|
|
966
|
+
if (pending !== void 0) await cancelPending(pending, "cancelled");
|
|
967
|
+
if (processing !== void 0) {
|
|
968
|
+
if (!processing.commitStarted) processing.controller.abort(new OAuthFlowError("cancelled", "The OAuth login was cancelled"));
|
|
969
|
+
processing = void 0;
|
|
970
|
+
}
|
|
971
|
+
await listenerClosing;
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
async function completeCallback(request) {
|
|
975
|
+
if (disposed) throw new OAuthFlowError("internal", "The OAuth flow is unavailable");
|
|
976
|
+
const candidate = pending;
|
|
977
|
+
if (candidate === void 0 || !candidate.active) throw noPendingError(currentStatus);
|
|
978
|
+
assertCallbackRequest(request);
|
|
979
|
+
const callback = parseCallback(request.url);
|
|
980
|
+
if (callback.state !== candidate.state) throw new OAuthFlowError("state-mismatch", "The OAuth callback state was not accepted");
|
|
981
|
+
candidate.active = false;
|
|
982
|
+
pending = void 0;
|
|
983
|
+
processing = candidate;
|
|
984
|
+
clock.clearTimeout(candidate.timeout);
|
|
985
|
+
closeListener(candidate);
|
|
986
|
+
if (callback.error !== void 0) {
|
|
987
|
+
processing = void 0;
|
|
988
|
+
candidate.controller.abort(new OAuthFlowError("oauth-error", "The OAuth provider rejected authorization"));
|
|
989
|
+
updateStatus(candidate, {
|
|
990
|
+
phase: "failed",
|
|
991
|
+
errorCode: "oauth-error"
|
|
992
|
+
});
|
|
993
|
+
return {
|
|
994
|
+
completed: false,
|
|
995
|
+
phase: "failed",
|
|
996
|
+
errorCode: "oauth-error"
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
if (callback.code === void 0) {
|
|
1000
|
+
processing = void 0;
|
|
1001
|
+
updateStatus(candidate, {
|
|
1002
|
+
phase: "failed",
|
|
1003
|
+
errorCode: "missing-code"
|
|
1004
|
+
});
|
|
1005
|
+
return {
|
|
1006
|
+
completed: false,
|
|
1007
|
+
phase: "failed",
|
|
1008
|
+
errorCode: "missing-code"
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
const operation = combineSignals(candidate.controller.signal);
|
|
1012
|
+
try {
|
|
1013
|
+
const token = await exchangeCode({
|
|
1014
|
+
code: callback.code,
|
|
1015
|
+
verifier: candidate.verifier,
|
|
1016
|
+
signal: operation.signal
|
|
1017
|
+
});
|
|
1018
|
+
if (operation.signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1019
|
+
assertToken(token);
|
|
1020
|
+
let project;
|
|
1021
|
+
try {
|
|
1022
|
+
project = await validateProject(token.accessToken, operation.signal);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
if (operation.signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1025
|
+
if (error instanceof ProjectDiscoveryError) {
|
|
1026
|
+
if (error.code === "cancelled") throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1027
|
+
throw new OAuthFlowError(projectErrorCode(error.code), projectErrorMessage(error.code));
|
|
1028
|
+
}
|
|
1029
|
+
throw error instanceof OAuthFlowError ? error : new OAuthFlowError("project-validation-failed", "Project validation failed");
|
|
1030
|
+
}
|
|
1031
|
+
if (operation.signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1032
|
+
if (project === void 0) throw new OAuthFlowError("project-unavailable", "No usable project is available for this account");
|
|
1033
|
+
project = normalizeProject(project);
|
|
1034
|
+
try {
|
|
1035
|
+
if (operation.signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1036
|
+
candidate.commitStarted = true;
|
|
1037
|
+
committingGeneration = candidate.generation;
|
|
1038
|
+
try {
|
|
1039
|
+
await commit(token, project, operation.signal);
|
|
1040
|
+
} finally {
|
|
1041
|
+
committingGeneration = void 0;
|
|
1042
|
+
}
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (operation.signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1045
|
+
throw error instanceof OAuthFlowError ? error : new OAuthFlowError("persistence-failed", "The login could not be saved");
|
|
1046
|
+
}
|
|
1047
|
+
updateStatus(candidate, { phase: "success" });
|
|
1048
|
+
return {
|
|
1049
|
+
completed: true,
|
|
1050
|
+
phase: "success"
|
|
1051
|
+
};
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
const safe = classifyCompletionError(error, candidate.controller.signal);
|
|
1054
|
+
if (safe.code === "cancelled") {
|
|
1055
|
+
updateStatus(candidate, {
|
|
1056
|
+
phase: "cancelled",
|
|
1057
|
+
errorCode: "cancelled"
|
|
1058
|
+
});
|
|
1059
|
+
return {
|
|
1060
|
+
completed: false,
|
|
1061
|
+
phase: "cancelled",
|
|
1062
|
+
errorCode: "cancelled"
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
updateStatus(candidate, {
|
|
1066
|
+
phase: "failed",
|
|
1067
|
+
errorCode: safe.code
|
|
1068
|
+
});
|
|
1069
|
+
return {
|
|
1070
|
+
completed: false,
|
|
1071
|
+
phase: "failed",
|
|
1072
|
+
errorCode: safe.code
|
|
1073
|
+
};
|
|
1074
|
+
} finally {
|
|
1075
|
+
operation.cleanup();
|
|
1076
|
+
if (processing === candidate) processing = void 0;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function handleLoopbackRequest(request) {
|
|
1080
|
+
try {
|
|
1081
|
+
const result = await completeCallback(request);
|
|
1082
|
+
if (result.completed) return callbackResponse(200, "success");
|
|
1083
|
+
return callbackResponse(result.phase === "cancelled" ? 409 : 400, result.errorCode);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
const safe = error instanceof OAuthFlowError ? error : new OAuthFlowError("internal", "The callback could not be processed");
|
|
1086
|
+
return callbackResponse(callbackStatus(safe.code), safe.code);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
async function expire(candidate) {
|
|
1090
|
+
if (pending !== candidate || !candidate.active) return;
|
|
1091
|
+
await cancelPending(candidate, "expired");
|
|
1092
|
+
}
|
|
1093
|
+
async function cancelPending(candidate, phase) {
|
|
1094
|
+
if (!candidate.active && pending !== candidate) return;
|
|
1095
|
+
candidate.active = false;
|
|
1096
|
+
if (pending === candidate) pending = void 0;
|
|
1097
|
+
clock.clearTimeout(candidate.timeout);
|
|
1098
|
+
candidate.controller.abort(new OAuthFlowError(phase, phase === "expired" ? "The OAuth login expired" : "The OAuth login was cancelled"));
|
|
1099
|
+
await closeListener(candidate);
|
|
1100
|
+
updateStatus(candidate, {
|
|
1101
|
+
phase,
|
|
1102
|
+
errorCode: phase
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
function updateStatus(candidate, status) {
|
|
1106
|
+
if (candidate.generation === generation) currentStatus = status;
|
|
1107
|
+
}
|
|
1108
|
+
return flow;
|
|
1109
|
+
}
|
|
1110
|
+
/** Create the production loopback listener; it never binds a non-loopback address. */
|
|
1111
|
+
function createNodeLoopbackListenerFactory() {
|
|
1112
|
+
return { listen: (handler) => new Promise((resolve, reject) => {
|
|
1113
|
+
const server = createServer((request, response) => {
|
|
1114
|
+
serveRequest(handler, request, response);
|
|
1115
|
+
});
|
|
1116
|
+
let settled = false;
|
|
1117
|
+
const onError = (error) => {
|
|
1118
|
+
if (!settled) {
|
|
1119
|
+
settled = true;
|
|
1120
|
+
reject(error);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
server.once("error", onError);
|
|
1124
|
+
server.listen(ANTIGRAVITY_CALLBACK_PORT, "127.0.0.1", () => {
|
|
1125
|
+
settled = true;
|
|
1126
|
+
server.removeListener("error", onError);
|
|
1127
|
+
resolve({ close: () => new Promise((resolveClose, rejectClose) => {
|
|
1128
|
+
if (!server.listening) {
|
|
1129
|
+
resolveClose();
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
server.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
|
|
1133
|
+
}) });
|
|
1134
|
+
});
|
|
1135
|
+
}) };
|
|
1136
|
+
}
|
|
1137
|
+
/** Build the authorization URL without ever putting the verifier in browser state. */
|
|
1138
|
+
function buildAuthorizationUrl(state, verifier) {
|
|
1139
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
1140
|
+
const url = new URL(AUTHORIZATION_ENDPOINT);
|
|
1141
|
+
url.searchParams.set("client_id", ANTIGRAVITY_CLIENT_ID);
|
|
1142
|
+
url.searchParams.set("response_type", "code");
|
|
1143
|
+
url.searchParams.set("redirect_uri", ANTIGRAVITY_REDIRECT_URI);
|
|
1144
|
+
url.searchParams.set("scope", ANTIGRAVITY_SCOPES.join(" "));
|
|
1145
|
+
url.searchParams.set("code_challenge", challenge);
|
|
1146
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
1147
|
+
url.searchParams.set("state", state);
|
|
1148
|
+
url.searchParams.set("access_type", "offline");
|
|
1149
|
+
url.searchParams.set("prompt", "consent");
|
|
1150
|
+
return url.toString();
|
|
1151
|
+
}
|
|
1152
|
+
/** Testable Google token exchange; response bodies are parsed only in Host memory. */
|
|
1153
|
+
function createGoogleTokenExchanger(fetchImpl, clock = systemClock()) {
|
|
1154
|
+
return async ({ code, verifier, signal }) => {
|
|
1155
|
+
const response = await fetchImpl(TOKEN_ENDPOINT, {
|
|
1156
|
+
method: "POST",
|
|
1157
|
+
headers: {
|
|
1158
|
+
Accept: "*/*",
|
|
1159
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
|
|
1160
|
+
},
|
|
1161
|
+
body: new URLSearchParams({
|
|
1162
|
+
client_id: ANTIGRAVITY_CLIENT_ID,
|
|
1163
|
+
client_secret: ANTIGRAVITY_CLIENT_SECRET,
|
|
1164
|
+
code,
|
|
1165
|
+
code_verifier: verifier,
|
|
1166
|
+
grant_type: "authorization_code",
|
|
1167
|
+
redirect_uri: ANTIGRAVITY_REDIRECT_URI
|
|
1168
|
+
}),
|
|
1169
|
+
signal
|
|
1170
|
+
});
|
|
1171
|
+
if (!response.ok) {
|
|
1172
|
+
try {
|
|
1173
|
+
await response.body?.cancel();
|
|
1174
|
+
} catch {}
|
|
1175
|
+
throw new OAuthFlowError("token-exchange-failed", "The authorization code could not be exchanged");
|
|
1176
|
+
}
|
|
1177
|
+
const payload = await readJsonBounded(response, signal);
|
|
1178
|
+
if (!isRecord$1(payload) || typeof payload.access_token !== "string" || payload.access_token.length === 0 || typeof payload.refresh_token !== "string" || payload.refresh_token.length === 0) throw new OAuthFlowError("token-exchange-failed", "The token response was not accepted");
|
|
1179
|
+
const expiresIn = typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) && payload.expires_in > 0 ? payload.expires_in : 3600;
|
|
1180
|
+
return {
|
|
1181
|
+
accessToken: payload.access_token,
|
|
1182
|
+
refreshToken: payload.refresh_token,
|
|
1183
|
+
expiresAt: clock.now() + expiresIn * 1e3,
|
|
1184
|
+
...typeof payload.email === "string" ? { email: payload.email } : {}
|
|
1185
|
+
};
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function assertCallbackRequest(request) {
|
|
1189
|
+
if (request.method !== "GET") throw new OAuthFlowError("invalid-method", "The OAuth callback method is not accepted");
|
|
1190
|
+
if (typeof request.host !== "string" || !isAllowedCallbackHost(request.host)) throw new OAuthFlowError("invalid-host", "The OAuth callback host is not accepted");
|
|
1191
|
+
let parsed;
|
|
1192
|
+
try {
|
|
1193
|
+
parsed = new URL(request.url, `http://${request.host}`);
|
|
1194
|
+
} catch {
|
|
1195
|
+
throw new OAuthFlowError("invalid-path", "The OAuth callback URL is not accepted");
|
|
1196
|
+
}
|
|
1197
|
+
if (parsed.protocol !== "http:" || !isAllowedCallbackHost(parsed.host) || parsed.username.length > 0 || parsed.password.length > 0 || parsed.pathname !== "/oauth-callback" || parsed.hash.length > 0) throw new OAuthFlowError("invalid-path", "The OAuth callback path is not accepted");
|
|
1198
|
+
}
|
|
1199
|
+
function parseCallback(value) {
|
|
1200
|
+
let parsed;
|
|
1201
|
+
try {
|
|
1202
|
+
parsed = new URL(value, `http://${ANTIGRAVITY_CALLBACK_HOSTS[0]}`);
|
|
1203
|
+
} catch {
|
|
1204
|
+
throw new OAuthFlowError("invalid-path", "The OAuth callback URL is not accepted");
|
|
1205
|
+
}
|
|
1206
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1207
|
+
for (const [key, paramValue] of parsed.searchParams) {
|
|
1208
|
+
if (seen.has(key)) throw new OAuthFlowError("duplicate-parameter", "The OAuth callback contains duplicate parameters");
|
|
1209
|
+
if (!safeCallbackValue(key) || !safeCallbackValue(paramValue)) throw new OAuthFlowError("invalid-parameters", "The OAuth callback parameters are not accepted");
|
|
1210
|
+
seen.add(key);
|
|
1211
|
+
}
|
|
1212
|
+
const state = parsed.searchParams.get("state");
|
|
1213
|
+
if (state === null || !safeCallbackValue(state)) throw new OAuthFlowError("missing-state", "The OAuth callback state is missing");
|
|
1214
|
+
const code = parsed.searchParams.get("code");
|
|
1215
|
+
const error = parsed.searchParams.get("error");
|
|
1216
|
+
if (error !== null) {
|
|
1217
|
+
if (code !== null || !safeCallbackValue(error)) throw new OAuthFlowError("invalid-parameters", "The OAuth callback parameters are not accepted");
|
|
1218
|
+
return {
|
|
1219
|
+
state,
|
|
1220
|
+
error
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
if (parsed.searchParams.has("error_description") || parsed.searchParams.has("error_uri")) throw new OAuthFlowError("invalid-parameters", "The OAuth callback parameters are not accepted");
|
|
1224
|
+
if (code === null || !safeCallbackValue(code)) throw new OAuthFlowError("missing-code", "The OAuth callback code is missing");
|
|
1225
|
+
return {
|
|
1226
|
+
state,
|
|
1227
|
+
code
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function isAllowedCallbackHost(value) {
|
|
1231
|
+
return ANTIGRAVITY_CALLBACK_HOSTS.includes(value.toLowerCase());
|
|
1232
|
+
}
|
|
1233
|
+
function safeCallbackValue(value) {
|
|
1234
|
+
return isBoundedSafeText(value, MAX_CALLBACK_VALUE_LENGTH);
|
|
1235
|
+
}
|
|
1236
|
+
function randomBytes$1(random, size) {
|
|
1237
|
+
const bytes = random(size);
|
|
1238
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== size) throw new OAuthFlowError("internal", "The OAuth random source is unavailable");
|
|
1239
|
+
return bytes;
|
|
1240
|
+
}
|
|
1241
|
+
function encodeBase64Url(bytes) {
|
|
1242
|
+
return Buffer.from(bytes).toString("base64url");
|
|
1243
|
+
}
|
|
1244
|
+
function systemClock() {
|
|
1245
|
+
return {
|
|
1246
|
+
now: () => Date.now(),
|
|
1247
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
1248
|
+
clearTimeout: (handle) => clearTimeout(handle)
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function noPendingError(status) {
|
|
1252
|
+
if (status.phase === "expired") return new OAuthFlowError("expired", "The OAuth login expired");
|
|
1253
|
+
if (status.phase === "cancelled") return new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1254
|
+
return new OAuthFlowError("no-pending-flow", "There is no pending OAuth login");
|
|
1255
|
+
}
|
|
1256
|
+
function asListenerError(error) {
|
|
1257
|
+
if (error instanceof OAuthFlowError) return error;
|
|
1258
|
+
if (isRecord$1(error) && error.code === "EADDRINUSE") return new OAuthFlowError("port-conflict", "The fixed OAuth callback port is already in use");
|
|
1259
|
+
return new OAuthFlowError("internal", "The OAuth callback listener could not start");
|
|
1260
|
+
}
|
|
1261
|
+
function classifyCompletionError(error, signal) {
|
|
1262
|
+
if (signal.aborted || error instanceof OAuthFlowError && error.code === "cancelled") return new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1263
|
+
if (error instanceof OAuthFlowError) return error;
|
|
1264
|
+
return new OAuthFlowError("token-exchange-failed", "The OAuth login could not be completed");
|
|
1265
|
+
}
|
|
1266
|
+
function assertToken(value) {
|
|
1267
|
+
if (!isRecord$1(value) || typeof value.accessToken !== "string" || value.accessToken.length === 0 || typeof value.refreshToken !== "string" || value.refreshToken.length === 0 || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) throw new OAuthFlowError("token-exchange-failed", "The token response was not accepted");
|
|
1268
|
+
}
|
|
1269
|
+
function normalizeProject(value) {
|
|
1270
|
+
if (!isRecord$1(value)) throw new OAuthFlowError("project-validation-failed", "Project validation failed");
|
|
1271
|
+
const projectId = normalizeProjectId(value.projectId);
|
|
1272
|
+
if (projectId === void 0) throw new OAuthFlowError("project-validation-failed", "Project validation failed");
|
|
1273
|
+
return {
|
|
1274
|
+
projectId,
|
|
1275
|
+
...typeof value.email === "string" ? { email: value.email } : {}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function projectErrorCode(code) {
|
|
1279
|
+
if (code === "authentication") return "project-authentication-failed";
|
|
1280
|
+
if (code === "forbidden") return "project-forbidden";
|
|
1281
|
+
if (code === "rate-limited") return "project-rate-limited";
|
|
1282
|
+
if (code === "offline") return "project-offline";
|
|
1283
|
+
if (code === "malformed") return "project-malformed";
|
|
1284
|
+
return "project-protocol-drift";
|
|
1285
|
+
}
|
|
1286
|
+
function projectErrorMessage(code) {
|
|
1287
|
+
if (code === "authentication") return "The Antigravity project probe requires authentication";
|
|
1288
|
+
if (code === "forbidden") return "The Antigravity project probe was forbidden";
|
|
1289
|
+
if (code === "rate-limited") return "The Antigravity project probe is rate-limited";
|
|
1290
|
+
if (code === "offline") return "The Antigravity project probe is offline";
|
|
1291
|
+
if (code === "malformed") return "The Antigravity project response was malformed";
|
|
1292
|
+
if (code === "protocol-drift") return "The Antigravity project protocol changed";
|
|
1293
|
+
return "The Antigravity project probe was cancelled";
|
|
1294
|
+
}
|
|
1295
|
+
function callbackResponse(status, outcome) {
|
|
1296
|
+
return {
|
|
1297
|
+
status,
|
|
1298
|
+
headers: EMPTY_RESPONSE_HEADERS,
|
|
1299
|
+
body: outcome === "success" ? "<!doctype html><meta charset=\"utf-8\"><title>Authorization complete</title><p>Authorization complete. You may return to DeepSeek Harness.</p>" : "<!doctype html><meta charset=\"utf-8\"><title>Authorization could not be completed</title><p>Authorization could not be completed. Return to DeepSeek Harness for details.</p>"
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function callbackStatus(code) {
|
|
1303
|
+
if (code === "port-conflict") return 409;
|
|
1304
|
+
if (code === "internal") return 500;
|
|
1305
|
+
return 400;
|
|
1306
|
+
}
|
|
1307
|
+
async function serveRequest(handler, request, response) {
|
|
1308
|
+
const host = typeof request.headers.host === "string" ? request.headers.host : void 0;
|
|
1309
|
+
const result = await handler({
|
|
1310
|
+
method: request.method ?? "",
|
|
1311
|
+
...host === void 0 ? {} : { host },
|
|
1312
|
+
url: request.url ?? ""
|
|
1313
|
+
});
|
|
1314
|
+
response.writeHead(result.status, result.headers);
|
|
1315
|
+
response.end(result.body);
|
|
1316
|
+
}
|
|
1317
|
+
async function readJsonBounded(response, signal) {
|
|
1318
|
+
if (response.body === null) {
|
|
1319
|
+
const text = await response.text();
|
|
1320
|
+
if (new TextEncoder().encode(text).byteLength > MAX_TOKEN_RESPONSE_BYTES) throw new OAuthFlowError("token-exchange-failed", "The token response was not accepted");
|
|
1321
|
+
try {
|
|
1322
|
+
return JSON.parse(text);
|
|
1323
|
+
} catch {
|
|
1324
|
+
throw new OAuthFlowError("token-exchange-failed", "The token response was not accepted");
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
const reader = response.body.getReader();
|
|
1328
|
+
const chunks = [];
|
|
1329
|
+
let total = 0;
|
|
1330
|
+
try {
|
|
1331
|
+
while (true) {
|
|
1332
|
+
if (signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1333
|
+
const next = await reader.read();
|
|
1334
|
+
if (next.done) break;
|
|
1335
|
+
total += next.value.byteLength;
|
|
1336
|
+
if (total > MAX_TOKEN_RESPONSE_BYTES) throw new OAuthFlowError("token-exchange-failed", "The token response was not accepted");
|
|
1337
|
+
chunks.push(next.value);
|
|
1338
|
+
}
|
|
1339
|
+
} finally {
|
|
1340
|
+
try {
|
|
1341
|
+
reader.releaseLock();
|
|
1342
|
+
} catch {}
|
|
1343
|
+
}
|
|
1344
|
+
const bytes = new Uint8Array(total);
|
|
1345
|
+
let offset = 0;
|
|
1346
|
+
for (const chunk of chunks) {
|
|
1347
|
+
bytes.set(chunk, offset);
|
|
1348
|
+
offset += chunk.byteLength;
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
return JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
1352
|
+
} catch {
|
|
1353
|
+
throw new OAuthFlowError("token-exchange-failed", "The token response was not accepted");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function combineSignals(primary) {
|
|
1357
|
+
const controller = new AbortController();
|
|
1358
|
+
const onAbort = () => {
|
|
1359
|
+
if (!controller.signal.aborted) controller.abort(primary.reason);
|
|
1360
|
+
};
|
|
1361
|
+
if (primary.aborted) controller.abort(primary.reason);
|
|
1362
|
+
else primary.addEventListener("abort", onAbort, { once: true });
|
|
1363
|
+
return {
|
|
1364
|
+
signal: controller.signal,
|
|
1365
|
+
cleanup: () => primary.removeEventListener("abort", onAbort)
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function isRecord$1(value) {
|
|
1369
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1370
|
+
}
|
|
1371
|
+
//#endregion
|
|
1372
|
+
//#region src/capability-gates.ts
|
|
1373
|
+
/** Value-free live-gate evidence used to keep capability registration honest. */
|
|
1374
|
+
const GATE_FILE_VERSION = 2;
|
|
1375
|
+
const MAX_GATE_FILE_BYTES = 65536;
|
|
1376
|
+
const PERSISTED_CAPABILITY_IDS = CAPABILITY_ROW_IDS.filter((id) => id !== "auth-llm");
|
|
1377
|
+
/** Resolve a gate record next to the plugin-owned auth record without reading either. */
|
|
1378
|
+
function defaultCapabilityGatePath(authStorePath) {
|
|
1379
|
+
return join(dirname(authStorePath), "gates.json");
|
|
1380
|
+
}
|
|
1381
|
+
function createMemoryCapabilityGates(initial = {}, now = () => Date.now()) {
|
|
1382
|
+
let current = cloneEvidence(initial);
|
|
1383
|
+
return {
|
|
1384
|
+
read: async () => cloneEvidence(current),
|
|
1385
|
+
recordGate0: async (subject, outcome) => {
|
|
1386
|
+
current = {
|
|
1387
|
+
subject: checkedSubject(subject),
|
|
1388
|
+
gate0: result(outcome, now)
|
|
1389
|
+
};
|
|
1390
|
+
return cloneEvidence(current);
|
|
1391
|
+
},
|
|
1392
|
+
recordLlmFamily: async (subject, family, outcome) => {
|
|
1393
|
+
current = evidenceForSubject(current, subject);
|
|
1394
|
+
current = {
|
|
1395
|
+
...current,
|
|
1396
|
+
llmFamilies: {
|
|
1397
|
+
...current.llmFamilies,
|
|
1398
|
+
[family]: result(outcome, now)
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
return cloneEvidence(current);
|
|
1402
|
+
},
|
|
1403
|
+
recordCapability: async (subject, id, outcome) => {
|
|
1404
|
+
current = evidenceForSubject(current, subject);
|
|
1405
|
+
current = {
|
|
1406
|
+
...current,
|
|
1407
|
+
capabilities: {
|
|
1408
|
+
...current.capabilities,
|
|
1409
|
+
[id]: result(outcome, now)
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
return cloneEvidence(current);
|
|
1413
|
+
},
|
|
1414
|
+
clear: async () => {
|
|
1415
|
+
current = {};
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
function createFileCapabilityGates(path, options = {}) {
|
|
1420
|
+
const now = options.now ?? (() => Date.now());
|
|
1421
|
+
let mutation = Promise.resolve();
|
|
1422
|
+
const read = async () => {
|
|
1423
|
+
let text;
|
|
1424
|
+
try {
|
|
1425
|
+
const [file, directory] = await Promise.all([lstat(path), lstat(dirname(path))]);
|
|
1426
|
+
if (file.isSymbolicLink() || !file.isFile() || directory.isSymbolicLink() || !directory.isDirectory() || file.size > MAX_GATE_FILE_BYTES || (file.mode & 63) !== 0 || (directory.mode & 63) !== 0) throw new Error("unsafe gate record");
|
|
1427
|
+
text = await readFile(path, "utf8");
|
|
1428
|
+
if (text.length > MAX_GATE_FILE_BYTES) throw new Error("oversized gate record");
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
if (error.code === "ENOENT") return {};
|
|
1431
|
+
throw new Error("The Antigravity capability gate record could not be read");
|
|
1432
|
+
}
|
|
1433
|
+
let value;
|
|
1434
|
+
try {
|
|
1435
|
+
value = JSON.parse(text);
|
|
1436
|
+
} catch {
|
|
1437
|
+
throw new Error("The Antigravity capability gate record is malformed");
|
|
1438
|
+
}
|
|
1439
|
+
return parseFile(value);
|
|
1440
|
+
};
|
|
1441
|
+
const update = async (mutate) => {
|
|
1442
|
+
let output = {};
|
|
1443
|
+
const operation = mutation.then(async () => {
|
|
1444
|
+
output = mutate(await read());
|
|
1445
|
+
await writeEvidence(path, output);
|
|
1446
|
+
});
|
|
1447
|
+
mutation = operation.catch(() => {});
|
|
1448
|
+
await operation;
|
|
1449
|
+
return cloneEvidence(output);
|
|
1450
|
+
};
|
|
1451
|
+
return {
|
|
1452
|
+
read,
|
|
1453
|
+
recordGate0: (subject, outcome) => update(() => ({
|
|
1454
|
+
subject: checkedSubject(subject),
|
|
1455
|
+
gate0: result(outcome, now)
|
|
1456
|
+
})),
|
|
1457
|
+
recordLlmFamily: (subject, family, outcome) => update((current) => {
|
|
1458
|
+
const owned = evidenceForSubject(current, subject);
|
|
1459
|
+
return {
|
|
1460
|
+
...owned,
|
|
1461
|
+
llmFamilies: {
|
|
1462
|
+
...owned.llmFamilies,
|
|
1463
|
+
[family]: result(outcome, now)
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
}),
|
|
1467
|
+
recordCapability: (subject, id, outcome) => update((current) => {
|
|
1468
|
+
const owned = evidenceForSubject(current, subject);
|
|
1469
|
+
return {
|
|
1470
|
+
...owned,
|
|
1471
|
+
capabilities: {
|
|
1472
|
+
...owned.capabilities,
|
|
1473
|
+
[id]: result(outcome, now)
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
}),
|
|
1477
|
+
clear: async () => {
|
|
1478
|
+
const operation = mutation.then(async () => {
|
|
1479
|
+
await rm(path, { force: true });
|
|
1480
|
+
});
|
|
1481
|
+
mutation = operation.catch(() => {});
|
|
1482
|
+
await operation;
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
async function writeEvidence(path, evidence) {
|
|
1487
|
+
const directory = dirname(path);
|
|
1488
|
+
await mkdir(directory, {
|
|
1489
|
+
recursive: true,
|
|
1490
|
+
mode: 448
|
|
1491
|
+
});
|
|
1492
|
+
await chmod(directory, 448);
|
|
1493
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1494
|
+
const encoded = `${JSON.stringify({
|
|
1495
|
+
version: GATE_FILE_VERSION,
|
|
1496
|
+
...evidence
|
|
1497
|
+
})}\n`;
|
|
1498
|
+
try {
|
|
1499
|
+
await writeFile(temporary, encoded, {
|
|
1500
|
+
encoding: "utf8",
|
|
1501
|
+
mode: 384,
|
|
1502
|
+
flag: "wx"
|
|
1503
|
+
});
|
|
1504
|
+
await rename(temporary, path);
|
|
1505
|
+
await chmod(path, 384);
|
|
1506
|
+
} finally {
|
|
1507
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function parseFile(value) {
|
|
1511
|
+
if (!isRecord(value) || value.version !== GATE_FILE_VERSION) throw new Error("The Antigravity capability gate record is malformed");
|
|
1512
|
+
if (Object.keys(value).some((key) => ![
|
|
1513
|
+
"version",
|
|
1514
|
+
"subject",
|
|
1515
|
+
"gate0",
|
|
1516
|
+
"llmFamilies",
|
|
1517
|
+
"capabilities"
|
|
1518
|
+
].includes(key))) throw new Error("The Antigravity capability gate record is malformed");
|
|
1519
|
+
if (typeof value.subject !== "string" || !isBoundedSafeText(value.subject, 128)) throw new Error("The Antigravity capability gate record is malformed");
|
|
1520
|
+
const gate0 = value.gate0 === void 0 ? void 0 : parseResult(value.gate0);
|
|
1521
|
+
const llmFamilies = value.llmFamilies === void 0 ? void 0 : parseLlmFamilies(value.llmFamilies);
|
|
1522
|
+
const capabilities = value.capabilities === void 0 ? void 0 : parseCapabilities(value.capabilities);
|
|
1523
|
+
return {
|
|
1524
|
+
subject: value.subject,
|
|
1525
|
+
...gate0 === void 0 ? {} : { gate0 },
|
|
1526
|
+
...llmFamilies === void 0 ? {} : { llmFamilies },
|
|
1527
|
+
...capabilities === void 0 ? {} : { capabilities }
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function parseLlmFamilies(value) {
|
|
1531
|
+
if (!isRecord(value) || Object.keys(value).some((key) => !LLM_FAMILY_IDS.includes(key))) throw new Error("The Antigravity capability gate record is malformed");
|
|
1532
|
+
const output = {};
|
|
1533
|
+
for (const family of LLM_FAMILY_IDS) if (value[family] !== void 0) output[family] = parseResult(value[family]);
|
|
1534
|
+
return output;
|
|
1535
|
+
}
|
|
1536
|
+
function parseCapabilities(value) {
|
|
1537
|
+
if (!isRecord(value) || Object.keys(value).some((key) => !PERSISTED_CAPABILITY_IDS.includes(key))) throw new Error("The Antigravity capability gate record is malformed");
|
|
1538
|
+
const output = {};
|
|
1539
|
+
for (const id of PERSISTED_CAPABILITY_IDS) if (value[id] !== void 0) output[id] = parseResult(value[id]);
|
|
1540
|
+
return output;
|
|
1541
|
+
}
|
|
1542
|
+
function parseResult(value) {
|
|
1543
|
+
if (!isRecord(value) || Object.keys(value).length !== 2 || !Object.prototype.hasOwnProperty.call(value, "outcome") || !Object.prototype.hasOwnProperty.call(value, "checkedAt") || typeof value.outcome !== "string" || !CAPABILITY_GATE_OUTCOMES.includes(value.outcome) || typeof value.checkedAt !== "string" || value.checkedAt.length > 64 || !Number.isFinite(Date.parse(value.checkedAt))) throw new Error("The Antigravity capability gate record is malformed");
|
|
1544
|
+
return {
|
|
1545
|
+
outcome: value.outcome,
|
|
1546
|
+
checkedAt: value.checkedAt
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
function result(outcome, now) {
|
|
1550
|
+
if (!CAPABILITY_GATE_OUTCOMES.includes(outcome)) throw new Error("The Antigravity capability gate outcome is invalid");
|
|
1551
|
+
const timestamp = now();
|
|
1552
|
+
if (!Number.isFinite(timestamp)) throw new Error("The Antigravity capability gate clock is invalid");
|
|
1553
|
+
return {
|
|
1554
|
+
outcome,
|
|
1555
|
+
checkedAt: new Date(timestamp).toISOString()
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function checkedSubject(subject) {
|
|
1559
|
+
if (!isBoundedSafeText(subject, 128)) throw new Error("The Antigravity capability gate subject is invalid");
|
|
1560
|
+
return subject;
|
|
1561
|
+
}
|
|
1562
|
+
function evidenceForSubject(current, subject) {
|
|
1563
|
+
const checked = checkedSubject(subject);
|
|
1564
|
+
return current.subject === checked ? current : { subject: checked };
|
|
1565
|
+
}
|
|
1566
|
+
function cloneEvidence(value) {
|
|
1567
|
+
return {
|
|
1568
|
+
...value.subject === void 0 ? {} : { subject: value.subject },
|
|
1569
|
+
...value.gate0 === void 0 ? {} : { gate0: { ...value.gate0 } },
|
|
1570
|
+
...value.llmFamilies === void 0 ? {} : { llmFamilies: Object.fromEntries(Object.entries(value.llmFamilies).map(([id, entry]) => [id, entry === void 0 ? void 0 : { ...entry }])) },
|
|
1571
|
+
...value.capabilities === void 0 ? {} : { capabilities: Object.fromEntries(Object.entries(value.capabilities).map(([id, entry]) => [id, entry === void 0 ? void 0 : { ...entry }])) }
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function isRecord(value) {
|
|
1575
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1576
|
+
}
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/auth-service.ts
|
|
1579
|
+
var AntigravityAuthService = class {
|
|
1580
|
+
store;
|
|
1581
|
+
credentials;
|
|
1582
|
+
flow;
|
|
1583
|
+
quota;
|
|
1584
|
+
gates;
|
|
1585
|
+
autoActivate;
|
|
1586
|
+
riskAcknowledged = false;
|
|
1587
|
+
activeFlowGeneration = 0;
|
|
1588
|
+
disposed = false;
|
|
1589
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
1590
|
+
constructor(options = {}) {
|
|
1591
|
+
this.autoActivate = options.autoActivateGates ?? false;
|
|
1592
|
+
const storePath = options.storePath ?? defaultAuthStorePath();
|
|
1593
|
+
this.store = options.store ?? createAuthStore(storePath);
|
|
1594
|
+
this.gates = options.gates ?? (options.gatePath !== void 0 ? createFileCapabilityGates(options.gatePath) : options.store === void 0 ? createFileCapabilityGates(defaultCapabilityGatePath(storePath)) : createMemoryCapabilityGates());
|
|
1595
|
+
this.credentials = createCredentialCoordinator({
|
|
1596
|
+
...options.credentialOptions,
|
|
1597
|
+
store: this.store
|
|
1598
|
+
});
|
|
1599
|
+
this.quota = createQuotaService({
|
|
1600
|
+
...options.quotaOptions,
|
|
1601
|
+
auth: this.credentials
|
|
1602
|
+
});
|
|
1603
|
+
const projectDiscovery = createProjectDiscovery(options.projectOptions);
|
|
1604
|
+
this.flow = createOAuthFlow({
|
|
1605
|
+
...options.flowOptions,
|
|
1606
|
+
validateProject: (accessToken, signal) => projectDiscovery.discover(accessToken, signal),
|
|
1607
|
+
commit: (token, project, signal) => this.commitCredential(token, project, signal)
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
async status() {
|
|
1611
|
+
const record = await this.readRecord();
|
|
1612
|
+
const flowStatus = this.flow.status();
|
|
1613
|
+
const phase = flowStatus.phase === "idle" && record !== void 0 ? "success" : flowStatus.phase;
|
|
1614
|
+
const maskedEmail = maskEmail(record?.email);
|
|
1615
|
+
const credentialStatus = await this.credentials.status();
|
|
1616
|
+
const gateEvidence = await this.gateEvidenceFor(record);
|
|
1617
|
+
const login = {
|
|
1618
|
+
phase,
|
|
1619
|
+
configured: record !== void 0,
|
|
1620
|
+
projectAvailable: record?.projectId !== void 0,
|
|
1621
|
+
...flowStatus.authorizationUrl === void 0 ? {} : { authorizationUrl: flowStatus.authorizationUrl },
|
|
1622
|
+
...flowStatus.expiresAt === void 0 ? {} : { expiresAt: flowStatus.expiresAt },
|
|
1623
|
+
...maskedEmail === void 0 ? {} : { maskedEmail },
|
|
1624
|
+
...flowStatus.errorCode === void 0 ? {} : { errorCode: flowStatus.errorCode }
|
|
1625
|
+
};
|
|
1626
|
+
return createStatusView(this.riskAcknowledged, login, credentialStatus, this.credentials.revokeStatus(), gateEvidence);
|
|
1627
|
+
}
|
|
1628
|
+
async acknowledgeRisk() {
|
|
1629
|
+
this.riskAcknowledged = true;
|
|
1630
|
+
this.notifyStatus();
|
|
1631
|
+
return { acknowledged: true };
|
|
1632
|
+
}
|
|
1633
|
+
/** Observe value-safe gate changes so capability rows can register without polling secrets. */
|
|
1634
|
+
watchStatus(listener) {
|
|
1635
|
+
this.statusListeners.add(listener);
|
|
1636
|
+
return () => {
|
|
1637
|
+
this.statusListeners.delete(listener);
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
async recordGate0(outcome) {
|
|
1641
|
+
const subject = gateSubject(await this.requireRecord());
|
|
1642
|
+
await this.gates.recordGate0(subject, outcome);
|
|
1643
|
+
this.notifyStatus();
|
|
1644
|
+
}
|
|
1645
|
+
async recordLlmFamilyGate(family, outcome) {
|
|
1646
|
+
const subject = gateSubject(await this.requireRecord());
|
|
1647
|
+
await this.gates.recordLlmFamily(subject, family, outcome);
|
|
1648
|
+
this.notifyStatus();
|
|
1649
|
+
}
|
|
1650
|
+
async recordCapabilityGate(id, outcome) {
|
|
1651
|
+
const subject = gateSubject(await this.requireRecord());
|
|
1652
|
+
if (id === "auth-llm") throw new OAuthFlowError("internal", "Auth/LLM availability is derived from independent family evidence");
|
|
1653
|
+
await this.gates.recordCapability(subject, id, outcome);
|
|
1654
|
+
this.notifyStatus();
|
|
1655
|
+
}
|
|
1656
|
+
async capabilityGateEvidence() {
|
|
1657
|
+
return this.gateEvidenceFor(await this.readRecord());
|
|
1658
|
+
}
|
|
1659
|
+
async gate0Passed() {
|
|
1660
|
+
return (await this.capabilityGateEvidence()).gate0?.outcome === "passed";
|
|
1661
|
+
}
|
|
1662
|
+
async capabilityAvailable(id) {
|
|
1663
|
+
return (await this.status()).capabilities.some((capability) => capability.id === id && capability.state === "available");
|
|
1664
|
+
}
|
|
1665
|
+
async startLogin() {
|
|
1666
|
+
if (this.disposed) throw new OAuthFlowError("internal", "The Antigravity login is unavailable");
|
|
1667
|
+
if (!this.riskAcknowledged) throw new OAuthFlowError("risk-acknowledgement-required", "Risk acknowledgement is required before login");
|
|
1668
|
+
const started = await this.flow.start();
|
|
1669
|
+
this.activeFlowGeneration = this.flow.generation();
|
|
1670
|
+
this.notifyStatus();
|
|
1671
|
+
return started;
|
|
1672
|
+
}
|
|
1673
|
+
async completeCallback(callbackUrl) {
|
|
1674
|
+
if (this.disposed) throw new OAuthFlowError("internal", "The Antigravity login is unavailable");
|
|
1675
|
+
try {
|
|
1676
|
+
return await this.flow.completeCallbackUrl(callbackUrl);
|
|
1677
|
+
} finally {
|
|
1678
|
+
this.notifyStatus();
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
async cancelLogin() {
|
|
1682
|
+
const status = await this.flow.cancel();
|
|
1683
|
+
if (status.phase !== "success") this.activeFlowGeneration = 0;
|
|
1684
|
+
this.notifyStatus();
|
|
1685
|
+
return {
|
|
1686
|
+
phase: status.phase,
|
|
1687
|
+
...status.errorCode === void 0 ? {} : { errorCode: status.errorCode }
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
async credential(signal, options) {
|
|
1691
|
+
return await this.credentials.credential(signal, options);
|
|
1692
|
+
}
|
|
1693
|
+
async usage(signal, force = false) {
|
|
1694
|
+
return await this.quota.refresh(signal, force);
|
|
1695
|
+
}
|
|
1696
|
+
async logout() {
|
|
1697
|
+
if (this.disposed) throw new OAuthFlowError("internal", "The Antigravity login is unavailable");
|
|
1698
|
+
this.activeFlowGeneration = 0;
|
|
1699
|
+
await this.flow.cancel();
|
|
1700
|
+
try {
|
|
1701
|
+
const result = await this.credentials.logout();
|
|
1702
|
+
await this.gates.clear();
|
|
1703
|
+
this.notifyStatus();
|
|
1704
|
+
return result;
|
|
1705
|
+
} catch {
|
|
1706
|
+
throw new OAuthFlowError("persistence-failed", "The local Antigravity credential could not be cleared");
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
async revoke(confirmed, signal) {
|
|
1710
|
+
if (this.disposed) throw new OAuthFlowError("internal", "The Antigravity login is unavailable");
|
|
1711
|
+
const result = await this.credentials.revoke(confirmed, signal);
|
|
1712
|
+
if (result.state === "revoked" || result.state === "logged-out" || result.state === "superseded") await this.gates.clear();
|
|
1713
|
+
this.notifyStatus();
|
|
1714
|
+
return result;
|
|
1715
|
+
}
|
|
1716
|
+
async dispose() {
|
|
1717
|
+
if (this.disposed) return;
|
|
1718
|
+
this.disposed = true;
|
|
1719
|
+
this.activeFlowGeneration = 0;
|
|
1720
|
+
this.statusListeners.clear();
|
|
1721
|
+
await Promise.all([
|
|
1722
|
+
this.flow.dispose(),
|
|
1723
|
+
this.credentials.dispose(),
|
|
1724
|
+
this.quota.dispose()
|
|
1725
|
+
]);
|
|
1726
|
+
}
|
|
1727
|
+
async commitCredential(token, project, signal) {
|
|
1728
|
+
const flowGeneration = this.flow.generation();
|
|
1729
|
+
if (this.disposed || flowGeneration !== this.activeFlowGeneration || signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1730
|
+
const current = await this.readRecord();
|
|
1731
|
+
if (this.disposed || flowGeneration !== this.activeFlowGeneration || signal.aborted) throw new OAuthFlowError("cancelled", "The OAuth login was cancelled");
|
|
1732
|
+
const email = maskEmail(project.email ?? token.email);
|
|
1733
|
+
const draft = {
|
|
1734
|
+
refreshToken: token.refreshToken,
|
|
1735
|
+
projectId: project.projectId,
|
|
1736
|
+
...email === void 0 ? {} : { email }
|
|
1737
|
+
};
|
|
1738
|
+
const committed = await this.store.compareAndCommit(current?.revision ?? 0, draft, current?.lineage);
|
|
1739
|
+
if (committed === void 0) throw new OAuthFlowError("credential-conflict", "The login changed while it was completing");
|
|
1740
|
+
if (this.autoActivate) {
|
|
1741
|
+
const subject = committed.lineage ?? "legacy-account";
|
|
1742
|
+
await this.autoActivateGates(subject);
|
|
1743
|
+
} else await this.gates.clear().catch(() => {});
|
|
1744
|
+
this.credentials.replaceFromLogin({
|
|
1745
|
+
accessToken: token.accessToken,
|
|
1746
|
+
refreshToken: token.refreshToken,
|
|
1747
|
+
expiresAt: token.expiresAt,
|
|
1748
|
+
projectId: project.projectId
|
|
1749
|
+
}, committed);
|
|
1750
|
+
}
|
|
1751
|
+
notifyStatus() {
|
|
1752
|
+
for (const listener of this.statusListeners) try {
|
|
1753
|
+
listener();
|
|
1754
|
+
} catch {}
|
|
1755
|
+
}
|
|
1756
|
+
async gateEvidenceFor(record) {
|
|
1757
|
+
try {
|
|
1758
|
+
const evidence = await this.gates.read();
|
|
1759
|
+
if (record === void 0) return {};
|
|
1760
|
+
const subject = gateSubject(record);
|
|
1761
|
+
if (evidence.subject === subject) return evidence;
|
|
1762
|
+
if (this.autoActivate && record.projectId !== void 0) {
|
|
1763
|
+
await this.autoActivateGates(subject);
|
|
1764
|
+
return await this.gates.read();
|
|
1765
|
+
}
|
|
1766
|
+
return {};
|
|
1767
|
+
} catch {
|
|
1768
|
+
return { gate0: {
|
|
1769
|
+
outcome: "protocol-drift",
|
|
1770
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1771
|
+
} };
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async autoActivateGates(subject) {
|
|
1775
|
+
try {
|
|
1776
|
+
await this.gates.recordGate0(subject, "passed");
|
|
1777
|
+
await this.gates.recordLlmFamily(subject, "gemini", "passed");
|
|
1778
|
+
await this.gates.recordLlmFamily(subject, "claude", "passed");
|
|
1779
|
+
await this.gates.recordLlmFamily(subject, "gpt-oss", "passed");
|
|
1780
|
+
await this.gates.recordCapability(subject, "search", "passed");
|
|
1781
|
+
await this.gates.recordCapability(subject, "image", "passed");
|
|
1782
|
+
await this.gates.recordCapability(subject, "video", "passed");
|
|
1783
|
+
} catch {}
|
|
1784
|
+
}
|
|
1785
|
+
async requireRecord() {
|
|
1786
|
+
const record = await this.readRecord();
|
|
1787
|
+
if (record === void 0) throw new OAuthFlowError("internal", "Antigravity login is required");
|
|
1788
|
+
return record;
|
|
1789
|
+
}
|
|
1790
|
+
async readRecord() {
|
|
1791
|
+
try {
|
|
1792
|
+
return await this.store.read();
|
|
1793
|
+
} catch {
|
|
1794
|
+
throw new OAuthFlowError("persistence-failed", "The Antigravity auth store could not be read");
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
function gateSubject(record) {
|
|
1799
|
+
return record.lineage ?? "legacy-account";
|
|
1800
|
+
}
|
|
1801
|
+
function createAntigravityAuthService(options = {}) {
|
|
1802
|
+
return new AntigravityAuthService(options);
|
|
1803
|
+
}
|
|
1804
|
+
function maskEmail(value) {
|
|
1805
|
+
if (!isBoundedSafeText(value, 4096)) return void 0;
|
|
1806
|
+
const at = value.indexOf("@");
|
|
1807
|
+
if (at <= 0 || at === value.length - 1) return void 0;
|
|
1808
|
+
const local = value.slice(0, at);
|
|
1809
|
+
const domain = value.slice(at + 1);
|
|
1810
|
+
if (!/^[^\s@]+$/u.test(local) || !/^[^\s@]+$/u.test(domain)) return void 0;
|
|
1811
|
+
return `${local.slice(0, 1)}***@${domain}`;
|
|
1812
|
+
}
|
|
1813
|
+
//#endregion
|
|
1814
|
+
//#region src/capability-lifecycle.ts
|
|
1815
|
+
/** Register a capability set atomically and dispose every owned member best-effort. */
|
|
1816
|
+
function registerCapabilitySet(values, register) {
|
|
1817
|
+
const disposers = [];
|
|
1818
|
+
const disposeAll = () => {
|
|
1819
|
+
for (const dispose of disposers.splice(0).reverse()) try {
|
|
1820
|
+
dispose();
|
|
1821
|
+
} catch {}
|
|
1822
|
+
};
|
|
1823
|
+
try {
|
|
1824
|
+
for (const value of values) disposers.push(register(value));
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
disposeAll();
|
|
1827
|
+
throw error;
|
|
1828
|
+
}
|
|
1829
|
+
return disposeAll;
|
|
1830
|
+
}
|
|
1831
|
+
function mountCapabilityLifecycle(options) {
|
|
1832
|
+
let gateReady = false;
|
|
1833
|
+
let registration;
|
|
1834
|
+
let generation = 0;
|
|
1835
|
+
let disposed = false;
|
|
1836
|
+
const sync = () => {
|
|
1837
|
+
if (disposed) return;
|
|
1838
|
+
const shouldRegister = options.enabled() && gateReady;
|
|
1839
|
+
if (shouldRegister && registration === void 0) try {
|
|
1840
|
+
registration = options.register();
|
|
1841
|
+
} catch {
|
|
1842
|
+
gateReady = false;
|
|
1843
|
+
}
|
|
1844
|
+
else if (!shouldRegister && registration !== void 0) {
|
|
1845
|
+
const dispose = registration;
|
|
1846
|
+
registration = void 0;
|
|
1847
|
+
try {
|
|
1848
|
+
dispose();
|
|
1849
|
+
} catch {}
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
const refresh = async () => {
|
|
1853
|
+
const currentGeneration = ++generation;
|
|
1854
|
+
let ready = false;
|
|
1855
|
+
try {
|
|
1856
|
+
const status = await options.auth.status();
|
|
1857
|
+
ready = status.login.projectAvailable && status.capabilities.some((capability) => capability.id === options.id && capability.state === "available");
|
|
1858
|
+
} catch {
|
|
1859
|
+
ready = false;
|
|
1860
|
+
}
|
|
1861
|
+
if (disposed || currentGeneration !== generation) return;
|
|
1862
|
+
gateReady = ready;
|
|
1863
|
+
sync();
|
|
1864
|
+
};
|
|
1865
|
+
const unwatch = options.auth.watchStatus?.(() => {
|
|
1866
|
+
refresh();
|
|
1867
|
+
}) ?? (() => {});
|
|
1868
|
+
options.ctx.effect?.(() => async () => {
|
|
1869
|
+
if (disposed) return;
|
|
1870
|
+
disposed = true;
|
|
1871
|
+
generation += 1;
|
|
1872
|
+
try {
|
|
1873
|
+
unwatch();
|
|
1874
|
+
} finally {
|
|
1875
|
+
const dispose = registration;
|
|
1876
|
+
registration = void 0;
|
|
1877
|
+
try {
|
|
1878
|
+
dispose?.();
|
|
1879
|
+
} finally {
|
|
1880
|
+
try {
|
|
1881
|
+
await options.cleanup?.();
|
|
1882
|
+
} finally {
|
|
1883
|
+
if (options.ownsAuth) await options.auth.dispose?.();
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}, options.label);
|
|
1888
|
+
sync();
|
|
1889
|
+
refresh();
|
|
1890
|
+
return {
|
|
1891
|
+
sync,
|
|
1892
|
+
refresh
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
//#endregion
|
|
1896
|
+
export { defaultAuthStorePath as _, maskEmail as a, defaultCapabilityGatePath as c, ANTIGRAVITY_TOKEN_ENDPOINT as d, CredentialOperationError as f, credentialErrorMessage as g, createGoogleRevokeTransport as h, createAntigravityAuthService as i, OAuthFlowError as l, createGoogleRefreshTransport as m, registerCapabilitySet as n, createFileCapabilityGates as o, createCredentialCoordinator as p, AntigravityAuthService as r, createMemoryCapabilityGates as s, mountCapabilityLifecycle as t, ANTIGRAVITY_REVOKE_ENDPOINT as u };
|