@indigoai-us/hq-cloud 5.1.9 → 5.1.10
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/dist/auth.js +2 -2
- package/dist/auth.js.map +1 -1
- package/dist/cli/accept.js +2 -2
- package/dist/cli/accept.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +23 -7
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +51 -13
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +6 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +31 -12
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +13 -2
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +18 -9
- package/dist/cognito-auth.js.map +1 -1
- package/dist/cognito-auth.test.d.ts +3 -3
- package/dist/cognito-auth.test.js +21 -10
- package/dist/cognito-auth.test.js.map +1 -1
- package/package.json +1 -1
- package/src/auth.ts +2 -2
- package/src/cli/accept.ts +2 -2
- package/src/cli/share.test.ts +59 -13
- package/src/cli/share.ts +25 -6
- package/src/cli/sync.test.ts +33 -12
- package/src/cli/sync.ts +6 -1
- package/src/cognito-auth.test.ts +22 -14
- package/src/cognito-auth.ts +31 -11
- package/test/invite-flow.integration.test.ts +1 -1
package/src/cognito-auth.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for cognito-auth.ts — focus on the `expiresAt` shape contract.
|
|
3
3
|
*
|
|
4
|
-
* Canonical on-disk shape is
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Canonical on-disk shape is epoch milliseconds (number). The reader also
|
|
5
|
+
* tolerates ISO 8601 strings for backward compatibility with pre-migration
|
|
6
|
+
* token files, and fails safe on anything unparseable.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as fs from "fs";
|
|
@@ -100,11 +100,21 @@ describe("isExpiring — expiresAt shape tolerance", () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
// ---------------------------------------------------------------------------
|
|
103
|
-
// Round-trip: writers emit
|
|
103
|
+
// Round-trip: writers emit epoch-ms, readers read epoch-ms
|
|
104
104
|
// ---------------------------------------------------------------------------
|
|
105
105
|
|
|
106
106
|
describe("expiresAt shape round-trip", () => {
|
|
107
|
-
it("saveCachedTokens + loadCachedTokens preserves
|
|
107
|
+
it("saveCachedTokens + loadCachedTokens preserves epoch-ms number shape", async () => {
|
|
108
|
+
const { saveCachedTokens, loadCachedTokens } = await importModule();
|
|
109
|
+
const epochMs = Date.now() + 3600 * 1000;
|
|
110
|
+
saveCachedTokens({ ...baseTokens, expiresAt: epochMs });
|
|
111
|
+
const loaded = loadCachedTokens();
|
|
112
|
+
expect(loaded).not.toBeNull();
|
|
113
|
+
expect(typeof loaded?.expiresAt).toBe("number");
|
|
114
|
+
expect(loaded?.expiresAt).toBe(epochMs);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("saveCachedTokens + loadCachedTokens tolerates legacy ISO string", async () => {
|
|
108
118
|
const { saveCachedTokens, loadCachedTokens } = await importModule();
|
|
109
119
|
const iso = new Date(Date.now() + 3600 * 1000).toISOString();
|
|
110
120
|
saveCachedTokens({ ...baseTokens, expiresAt: iso });
|
|
@@ -112,12 +122,9 @@ describe("expiresAt shape round-trip", () => {
|
|
|
112
122
|
expect(loaded).not.toBeNull();
|
|
113
123
|
expect(typeof loaded?.expiresAt).toBe("string");
|
|
114
124
|
expect(loaded?.expiresAt).toBe(iso);
|
|
115
|
-
expect(loaded?.expiresAt).toMatch(
|
|
116
|
-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/,
|
|
117
|
-
);
|
|
118
125
|
});
|
|
119
126
|
|
|
120
|
-
it("refreshTokens writes
|
|
127
|
+
it("refreshTokens writes epoch milliseconds to cache", async () => {
|
|
121
128
|
vi.stubGlobal(
|
|
122
129
|
"fetch",
|
|
123
130
|
vi.fn(async () =>
|
|
@@ -135,6 +142,7 @@ describe("expiresAt shape round-trip", () => {
|
|
|
135
142
|
);
|
|
136
143
|
|
|
137
144
|
const { refreshTokens, loadCachedTokens } = await importModule();
|
|
145
|
+
const before = Date.now();
|
|
138
146
|
const result = await refreshTokens(
|
|
139
147
|
{
|
|
140
148
|
region: "us-east-1",
|
|
@@ -143,14 +151,14 @@ describe("expiresAt shape round-trip", () => {
|
|
|
143
151
|
},
|
|
144
152
|
"prior-refresh-token",
|
|
145
153
|
);
|
|
154
|
+
const after = Date.now();
|
|
146
155
|
|
|
147
|
-
expect(typeof result.expiresAt).toBe("
|
|
148
|
-
expect(result.expiresAt).
|
|
149
|
-
|
|
150
|
-
);
|
|
156
|
+
expect(typeof result.expiresAt).toBe("number");
|
|
157
|
+
expect(result.expiresAt).toBeGreaterThanOrEqual(before + 3600 * 1000);
|
|
158
|
+
expect(result.expiresAt).toBeLessThanOrEqual(after + 3600 * 1000);
|
|
151
159
|
|
|
152
160
|
const onDisk = loadCachedTokens();
|
|
153
161
|
expect(onDisk?.expiresAt).toBe(result.expiresAt);
|
|
154
|
-
expect(typeof onDisk?.expiresAt).toBe("
|
|
162
|
+
expect(typeof onDisk?.expiresAt).toBe("number");
|
|
155
163
|
});
|
|
156
164
|
});
|
package/src/cognito-auth.ts
CHANGED
|
@@ -38,14 +38,25 @@ export interface CognitoAuthConfig {
|
|
|
38
38
|
port?: number;
|
|
39
39
|
/** OAuth scopes. Defaults to ["openid", "email", "profile"]. */
|
|
40
40
|
scopes?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Force a federated IdP (e.g. "Google"). When set, the Hosted UI IdP picker
|
|
43
|
+
* is bypassed and Cognito redirects straight to the provider. When omitted,
|
|
44
|
+
* Cognito shows its default picker.
|
|
45
|
+
*/
|
|
46
|
+
identityProvider?: string;
|
|
47
|
+
/**
|
|
48
|
+
* OAuth `prompt` param (e.g. "select_account"). Only meaningful when the IdP
|
|
49
|
+
* honors it — Google uses it to force account re-selection.
|
|
50
|
+
*/
|
|
51
|
+
prompt?: string;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export interface CognitoTokens {
|
|
44
55
|
accessToken: string;
|
|
45
56
|
idToken: string;
|
|
46
57
|
refreshToken: string;
|
|
47
|
-
/**
|
|
48
|
-
expiresAt: string;
|
|
58
|
+
/** Epoch milliseconds when the access token expires. Writers MUST emit a number. Readers accept ISO 8601 strings for backward compatibility with pre-migration token files. */
|
|
59
|
+
expiresAt: string | number;
|
|
49
60
|
tokenType: "Bearer";
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -78,7 +89,9 @@ export function saveCachedTokens(tokens: CognitoTokens): void {
|
|
|
78
89
|
if (!fs.existsSync(HQ_DIR)) {
|
|
79
90
|
fs.mkdirSync(HQ_DIR, { recursive: true, mode: 0o700 });
|
|
80
91
|
}
|
|
81
|
-
|
|
92
|
+
const tmpPath = path.join(HQ_DIR, `.cognito-tokens.json.tmp.${process.pid}`);
|
|
93
|
+
fs.writeFileSync(tmpPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
94
|
+
fs.renameSync(tmpPath, TOKEN_FILE);
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
export function clearCachedTokens(): void {
|
|
@@ -86,11 +99,10 @@ export function clearCachedTokens(): void {
|
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
/**
|
|
89
|
-
* Parse `expiresAt` to epoch-ms. Canonical on-disk shape is
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* as "expired" and force a refresh.
|
|
102
|
+
* Parse `expiresAt` to epoch-ms. Canonical on-disk shape is epoch milliseconds
|
|
103
|
+
* (number). Older token files may contain ISO 8601 strings. Accept both for
|
|
104
|
+
* migration safety. Returns null for anything unparseable — callers should
|
|
105
|
+
* treat that as "expired" and force a refresh.
|
|
94
106
|
*/
|
|
95
107
|
function parseExpiresAtMs(raw: unknown): number | null {
|
|
96
108
|
if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
|
|
@@ -158,7 +170,9 @@ export async function browserLogin(
|
|
|
158
170
|
const { verifier, challenge } = generatePkce();
|
|
159
171
|
const state = base64UrlEncode(crypto.randomBytes(16));
|
|
160
172
|
|
|
161
|
-
|
|
173
|
+
// Use `/oauth2/authorize` (not `/login`) so `identity_provider` + `prompt`
|
|
174
|
+
// are honored. `/login` ignores those params and always shows the IdP picker.
|
|
175
|
+
const authUrl = new URL(`${authBaseUrl(config)}/oauth2/authorize`);
|
|
162
176
|
authUrl.searchParams.set("client_id", config.clientId);
|
|
163
177
|
authUrl.searchParams.set("response_type", "code");
|
|
164
178
|
authUrl.searchParams.set("scope", scopes);
|
|
@@ -166,6 +180,12 @@ export async function browserLogin(
|
|
|
166
180
|
authUrl.searchParams.set("code_challenge", challenge);
|
|
167
181
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
168
182
|
authUrl.searchParams.set("state", state);
|
|
183
|
+
if (config.identityProvider) {
|
|
184
|
+
authUrl.searchParams.set("identity_provider", config.identityProvider);
|
|
185
|
+
}
|
|
186
|
+
if (config.prompt) {
|
|
187
|
+
authUrl.searchParams.set("prompt", config.prompt);
|
|
188
|
+
}
|
|
169
189
|
|
|
170
190
|
const code = await waitForAuthCode(port, state);
|
|
171
191
|
const tokens = await exchangeCodeForTokens(config, code, verifier, port);
|
|
@@ -300,7 +320,7 @@ async function exchangeCodeForTokens(
|
|
|
300
320
|
accessToken: data.access_token,
|
|
301
321
|
idToken: data.id_token,
|
|
302
322
|
refreshToken: data.refresh_token,
|
|
303
|
-
expiresAt:
|
|
323
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
304
324
|
tokenType: "Bearer",
|
|
305
325
|
};
|
|
306
326
|
}
|
|
@@ -336,7 +356,7 @@ export async function refreshTokens(
|
|
|
336
356
|
accessToken: data.access_token,
|
|
337
357
|
idToken: data.id_token,
|
|
338
358
|
refreshToken: data.refresh_token ?? currentRefreshToken,
|
|
339
|
-
expiresAt:
|
|
359
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
340
360
|
tokenType: "Bearer",
|
|
341
361
|
};
|
|
342
362
|
saveCachedTokens(tokens);
|
|
@@ -57,7 +57,7 @@ describe("parseToken", () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it("extracts token from https:// URL", () => {
|
|
60
|
-
expect(parseToken("https://
|
|
60
|
+
expect(parseToken("https://example.com/accept/tok_xyz")).toBe("tok_xyz");
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
it("returns raw token unchanged", () => {
|