primcli 1.74.1 → 3.0.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -72
- package/bin/claude-pending-mail.mjs +1 -1
- package/bin/run.js +14 -0
- package/dist/{api-command-fKP3NYa7.js → api-command-ctHBqTL5.js} +825 -95
- package/dist/{cli-config-C0Y_Ppol.js → cli-config-Ms_0vNjq.js} +66 -28
- package/dist/{client.gen-CaaMP86o.js → client.gen-6rZvmHfx.js} +7 -14
- package/dist/{json-output-Ce10AsSb.js → json-output-BzSBR-KX.js} +1 -2
- package/dist/{listen-background-cD6n8VYO.js → listen-background-C0F9gNDD.js} +2 -3
- package/dist/{machine-session-B6V5-grn.js → machine-session-Cp_eQ9t_.js} +164 -99
- package/dist/{notify-session-DJAb9SGm.js → notify-session-BN0JTPZ3.js} +341 -31
- package/dist/oclif/auto-signal-worker.js +5 -6
- package/dist/oclif/auto-signals.js +4 -5
- package/dist/oclif/global-org-flag.js +76 -0
- package/dist/oclif/help.js +127 -0
- package/dist/oclif/index.js +93239 -31954
- package/dist/oclif/listen-supervisor.js +1 -1
- package/dist/oclif/proxy-auto-detect.js +2 -3
- package/dist/oclif/receiver-heal.js +6 -6
- package/dist/oclif/root-signup-hint.js +3 -4
- package/dist/receiver.d.ts +95 -0
- package/dist/receiver.js +569 -0
- package/dist/{repeats-BL7ZC-_6-B_ESsWTH.js → repeats-Dj_o6c-X-Bf9G0xds.js} +4617 -1744
- package/dist/{scoped-chat-DAiOb622.js → scoped-chat-DZ42V3jq.js} +3 -5
- package/dist/{shared-mail-files-CZfm7E2k.js → shared-mail-files-BAQSGxxz.js} +2 -4
- package/dist/{shared-mail-watch-DyQB0MK0.js → shared-mail-watch-BAPMsFNv.js} +2 -3
- package/dist/{signal-command-Cte4mAL3.js → signal-command-CRUMUCNa.js} +19 -27
- package/dist/skills/primitive-connect/references/connect.md +3 -1
- package/dist/skills/primitive-connect/references/contacts.md +9 -4
- package/dist/skills/primitive-connect.json +4 -4
- package/man/primitive.1 +44 -3
- package/package.json +65 -15
|
@@ -1,11 +1,603 @@
|
|
|
1
|
-
import { $ as AutomatedFilterUnsupportedError,
|
|
2
|
-
import { C as
|
|
3
|
-
import
|
|
1
|
+
import { $ as AutomatedFilterUnsupportedError, An as sdk_gen_exports, F as assertAwaitingFilterRows, G as WEBHOOK_EVENT_TYPES, I as awaitingRejectedError, It as getEmail, Kt as getThread, L as ensureSearchReportsReplyState, P as ReplyStateUnsupportedError, U as replyStateSurfaceForOperation, V as isAwaitingRejectedError, W as SENT_EMAIL_EVENT_TYPES, et as assertAutomatedVerdict, it as expectedAutomatedVerdicts, nt as automatedSurfaceForOperation, on as listEndpoints, ot as isAutomatedRejectedError, rt as ensureSearchReportsAutomated, tt as automatedRejectedError, v as parseRetryAfter } from "./repeats-Dj_o6c-X-Bf9G0xds.js";
|
|
2
|
+
import { C as saveCliCredentials, F as agentProfilesDirectory, H as requireDefaultLoginProfile, N as agentProfileDirectory, R as loadConnectedAgentProfile, S as resolveCliAuth, W as PrimitiveApiClient, _ as deleteCliCredentials, b as loadCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, i as loadCliConfig, j as AGENT_PROFILE_ENV, m as cliAccessTokenExpiresAt, p as acquireCliCredentialsLock, w as saveSignupCredentials, x as normalizeApiBaseUrl, y as detectPrimitiveKeyEnvMisname } from "./cli-config-Ms_0vNjq.js";
|
|
3
|
+
import "./oclif/global-org-flag.js";
|
|
4
|
+
import { d as readMailJson, h as writeMailJson, m as withMailLock, p as syncMailDirectory, u as privateMailPermissions } from "./shared-mail-files-BAQSGxxz.js";
|
|
4
5
|
import { Command, Errors, Flags } from "@oclif/core";
|
|
5
6
|
import { existsSync, lstatSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
8
|
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
+
// src/oclif/user-grant.ts
|
|
10
|
+
/**
|
|
11
|
+
* CLI user logins: a login as the person, not bound to one organization,
|
|
12
|
+
* that switches between the person's organizations without logging in again.
|
|
13
|
+
*
|
|
14
|
+
* The saved login holds the person's identity token (`prim_uat_`, short
|
|
15
|
+
* lived) and refresh token (`prim_urt_`). Neither calls the API: the
|
|
16
|
+
* identity token only lists organizations (`GET /v1/cli/orgs`) and is
|
|
17
|
+
* exchanged (RFC 8693, at `/oauth/token`) for a short-lived access token in
|
|
18
|
+
* one organization. That per-org token is what every other command sends.
|
|
19
|
+
* It cannot be refreshed, so the CLI exchanges again when it expires or
|
|
20
|
+
* when the API answers 401.
|
|
21
|
+
*
|
|
22
|
+
* Saved logins made by older CLIs, and logins against servers without this
|
|
23
|
+
* feature, keep their org-bound OAuth format (`auth_method: "oauth"`) and
|
|
24
|
+
* never reach this module.
|
|
25
|
+
*/
|
|
26
|
+
const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
27
|
+
const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
28
|
+
/** Exchange or refresh this long before a token's stated expiry. */
|
|
29
|
+
const TOKEN_SKEW_MS = 6e4;
|
|
30
|
+
/** Cached tokens for organizations other than the current one. */
|
|
31
|
+
const MAX_CACHED_ORG_TOKENS = 20;
|
|
32
|
+
const USER_LOGIN_EXPIRED_MESSAGE = "Saved Primitive CLI login expired or was revoked. Run `primitive login` to sign in again.";
|
|
33
|
+
const NO_CURRENT_ORG_MESSAGE = "No organization is selected for this login. Run `primitive orgs` to see where you can use the CLI, then `primitive org use <org>`.";
|
|
34
|
+
const REFUSAL_FALLBACK_MESSAGES = {
|
|
35
|
+
not_a_member: "You are not a member of this organization.",
|
|
36
|
+
cli_access_disabled_by_org: "This organization does not allow CLI access.",
|
|
37
|
+
role_insufficient: "Your role in this organization does not allow CLI access. Ask an owner or admin to allow CLI access for members.",
|
|
38
|
+
cli_access_revoked: "Your CLI access to this organization was revoked. Run `primitive login` again to sign in afresh.",
|
|
39
|
+
grant_not_available: "The requested grant is not available for CLI logins on this server."
|
|
40
|
+
};
|
|
41
|
+
/** A refusal from the exchange or the org list, with the server's reason. */
|
|
42
|
+
var CliOrgRefusedError = class extends Errors.CLIError {
|
|
43
|
+
reason;
|
|
44
|
+
orgId;
|
|
45
|
+
constructor(params) {
|
|
46
|
+
super(params.message, {
|
|
47
|
+
code: params.reason,
|
|
48
|
+
exit: 1
|
|
49
|
+
});
|
|
50
|
+
this.reason = params.reason;
|
|
51
|
+
this.orgId = params.orgId;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function apiOrigin(apiBaseUrl) {
|
|
58
|
+
const url = new URL(apiBaseUrl);
|
|
59
|
+
url.search = "";
|
|
60
|
+
url.hash = "";
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
function oauthEndpoint(apiBaseUrl, path) {
|
|
64
|
+
const url = apiOrigin(apiBaseUrl);
|
|
65
|
+
url.pathname = path;
|
|
66
|
+
return url.toString();
|
|
67
|
+
}
|
|
68
|
+
function cliOrgsEndpoint(apiBaseUrl) {
|
|
69
|
+
const url = apiOrigin(apiBaseUrl);
|
|
70
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/cli/orgs`;
|
|
71
|
+
return url.toString();
|
|
72
|
+
}
|
|
73
|
+
function expiringSoon(expiresAt, now) {
|
|
74
|
+
const at = Date.parse(expiresAt);
|
|
75
|
+
return !Number.isFinite(at) || at <= now() + TOKEN_SKEW_MS;
|
|
76
|
+
}
|
|
77
|
+
function isUuid(value) {
|
|
78
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
79
|
+
}
|
|
80
|
+
function isOrgAccessToken(value) {
|
|
81
|
+
return isRecord(value) && typeof value.access_token === "string" && value.access_token.length > 0 && value.token_type === "Bearer" && typeof value.expires_in === "number" && value.expires_in > 0 && typeof value.org_id === "string" && (value.org_name === null || typeof value.org_name === "string") && typeof value.oauth_grant_id === "string";
|
|
82
|
+
}
|
|
83
|
+
function orgTokenFromResponse(token, now = Date.now) {
|
|
84
|
+
return {
|
|
85
|
+
access_token: token.access_token,
|
|
86
|
+
expires_at: cliAccessTokenExpiresAt(token.expires_in, now),
|
|
87
|
+
oauth_grant_id: token.oauth_grant_id,
|
|
88
|
+
org_id: token.org_id,
|
|
89
|
+
org_name: token.org_name ?? null,
|
|
90
|
+
role: typeof token.role === "string" ? token.role : null
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The org-bound view every other command reads: the per-org token in the
|
|
95
|
+
* standard fields, the person's grant kept alongside.
|
|
96
|
+
*/
|
|
97
|
+
function credentialsForOrg(credentials, org) {
|
|
98
|
+
return {
|
|
99
|
+
...credentials,
|
|
100
|
+
access_token: org.access_token,
|
|
101
|
+
expires_at: org.expires_at,
|
|
102
|
+
oauth_grant_id: org.oauth_grant_id,
|
|
103
|
+
org_id: org.org_id,
|
|
104
|
+
org_name: org.org_name
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function requireUserGrant(credentials) {
|
|
108
|
+
if (credentials.auth_method !== "user_grant" || !credentials.user_grant) throw new Errors.CLIError("This saved login is bound to one organization. Run `primitive logout`, then `primitive login`, to sign in as yourself and switch between organizations.", { exit: 1 });
|
|
109
|
+
return credentials.user_grant;
|
|
110
|
+
}
|
|
111
|
+
async function postTokenEndpoint(params) {
|
|
112
|
+
const fetchImpl = params.fetch ?? fetch;
|
|
113
|
+
let response;
|
|
114
|
+
try {
|
|
115
|
+
response = await fetchImpl(oauthEndpoint(params.apiBaseUrl, params.path ?? "/oauth/token"), {
|
|
116
|
+
body: new URLSearchParams(params.body),
|
|
117
|
+
headers: {
|
|
118
|
+
...params.headers ?? {},
|
|
119
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
120
|
+
},
|
|
121
|
+
method: "POST"
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
125
|
+
throw new Errors.CLIError(`Could not reach the Primitive OAuth token endpoint: ${detail}`, { exit: 1 });
|
|
126
|
+
}
|
|
127
|
+
const payload = await response.json().catch(() => null);
|
|
128
|
+
if (response.ok && isRecord(payload)) return {
|
|
129
|
+
ok: true,
|
|
130
|
+
payload
|
|
131
|
+
};
|
|
132
|
+
if (response.ok && params.allowEmptySuccess) return {
|
|
133
|
+
ok: true,
|
|
134
|
+
payload: {}
|
|
135
|
+
};
|
|
136
|
+
const record = isRecord(payload) ? payload : {};
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
status: response.status,
|
|
140
|
+
error: typeof record.error === "string" ? record.error : null,
|
|
141
|
+
description: typeof record.error_description === "string" ? record.error_description : null,
|
|
142
|
+
reason: typeof record.reason === "string" ? record.reason : null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Refresh the identity token when it is about to expire. The refresh token
|
|
147
|
+
* rotates on every use, so the new pair is saved before it is used.
|
|
148
|
+
* `invalid_grant` means the login is gone: the saved login is removed.
|
|
149
|
+
*/
|
|
150
|
+
async function freshIdentity(params) {
|
|
151
|
+
const grant = requireUserGrant(params.credentials);
|
|
152
|
+
if (!params.force && !expiringSoon(grant.expires_at, params.now)) return params.credentials;
|
|
153
|
+
const result = await postTokenEndpoint({
|
|
154
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
155
|
+
body: {
|
|
156
|
+
client_id: params.credentials.oauth_client_id,
|
|
157
|
+
grant_type: "refresh_token",
|
|
158
|
+
refresh_token: params.credentials.refresh_token
|
|
159
|
+
},
|
|
160
|
+
fetch: params.fetch,
|
|
161
|
+
headers: params.headers
|
|
162
|
+
});
|
|
163
|
+
if (!result.ok) {
|
|
164
|
+
if (result.error === "invalid_grant") {
|
|
165
|
+
deleteCliCredentials(params.configDir);
|
|
166
|
+
throw new Errors.CLIError(USER_LOGIN_EXPIRED_MESSAGE, { exit: 1 });
|
|
167
|
+
}
|
|
168
|
+
throw new Errors.CLIError(`Could not refresh the saved Primitive CLI login${result.description ? `: ${result.description}` : "."}`, { exit: 1 });
|
|
169
|
+
}
|
|
170
|
+
const payload = result.payload;
|
|
171
|
+
if (typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.expires_in !== "number" || payload.expires_in <= 0) throw new Errors.CLIError("Primitive OAuth token endpoint returned an unexpected refresh response.", { exit: 1 });
|
|
172
|
+
const next = {
|
|
173
|
+
...params.credentials,
|
|
174
|
+
refresh_token: payload.refresh_token,
|
|
175
|
+
user_grant: {
|
|
176
|
+
...grant,
|
|
177
|
+
access_token: payload.access_token,
|
|
178
|
+
expires_at: cliAccessTokenExpiresAt(payload.expires_in, params.now)
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
saveCliCredentials(params.configDir, next);
|
|
182
|
+
return next;
|
|
183
|
+
}
|
|
184
|
+
function refusalMessage(reason, description) {
|
|
185
|
+
return description ?? (reason ? REFUSAL_FALLBACK_MESSAGES[reason] : void 0) ?? "Primitive refused access to this organization.";
|
|
186
|
+
}
|
|
187
|
+
/** One exchange of the identity token for an access token in `orgId`. */
|
|
188
|
+
async function exchangeForOrg(params) {
|
|
189
|
+
const body = {
|
|
190
|
+
client_id: params.clientId,
|
|
191
|
+
grant_type: TOKEN_EXCHANGE_GRANT_TYPE,
|
|
192
|
+
org: params.orgId,
|
|
193
|
+
subject_token: params.identityToken,
|
|
194
|
+
subject_token_type: ACCESS_TOKEN_TYPE
|
|
195
|
+
};
|
|
196
|
+
if (params.requestedGrant) body.requested_grant = params.requestedGrant;
|
|
197
|
+
const result = await postTokenEndpoint({
|
|
198
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
199
|
+
body,
|
|
200
|
+
fetch: params.fetch,
|
|
201
|
+
headers: params.headers
|
|
202
|
+
});
|
|
203
|
+
if (!result.ok) return result;
|
|
204
|
+
if (!isOrgAccessToken(result.payload)) throw new Errors.CLIError("Primitive OAuth token endpoint returned an unexpected token exchange response.", { exit: 1 });
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
token: result.payload
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Exchange for `orgId`, refreshing the identity token first when needed and
|
|
212
|
+
* once more if the exchange says it expired. Saves the result: as the
|
|
213
|
+
* current organization when `makeCurrent`, otherwise in the per-org cache.
|
|
214
|
+
* Must run while the credentials lock is held.
|
|
215
|
+
*/
|
|
216
|
+
async function exchangeAndSave(params) {
|
|
217
|
+
let credentials = await freshIdentity(params);
|
|
218
|
+
let attempt = 0;
|
|
219
|
+
for (;;) {
|
|
220
|
+
const grant = requireUserGrant(credentials);
|
|
221
|
+
const result = await exchangeForOrg({
|
|
222
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
223
|
+
clientId: credentials.oauth_client_id,
|
|
224
|
+
fetch: params.fetch,
|
|
225
|
+
headers: params.headers,
|
|
226
|
+
identityToken: grant.access_token,
|
|
227
|
+
orgId: params.orgId,
|
|
228
|
+
requestedGrant: grant.requested_grant ?? null
|
|
229
|
+
});
|
|
230
|
+
if (result.ok) {
|
|
231
|
+
const org = orgTokenFromResponse(result.token, params.now);
|
|
232
|
+
const saved = saveOrgToken(credentials, org, params.makeCurrent);
|
|
233
|
+
saveCliCredentials(params.configDir, saved);
|
|
234
|
+
return {
|
|
235
|
+
credentials: saved,
|
|
236
|
+
org
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (result.error === "invalid_grant" && attempt === 0) {
|
|
240
|
+
attempt += 1;
|
|
241
|
+
credentials = await freshIdentity({
|
|
242
|
+
...params,
|
|
243
|
+
credentials,
|
|
244
|
+
force: true
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (result.error === "invalid_grant") {
|
|
249
|
+
deleteCliCredentials(params.configDir);
|
|
250
|
+
throw new Errors.CLIError(USER_LOGIN_EXPIRED_MESSAGE, { exit: 1 });
|
|
251
|
+
}
|
|
252
|
+
if (result.error === "unsupported_grant_type") throw new Errors.CLIError("This Primitive server does not support switching organizations for CLI logins. Run `primitive logout`, then `primitive login`, to sign in to one organization.", { exit: 1 });
|
|
253
|
+
if (result.reason) throw new CliOrgRefusedError({
|
|
254
|
+
message: refusalMessage(result.reason, result.description),
|
|
255
|
+
orgId: params.orgId,
|
|
256
|
+
reason: result.reason
|
|
257
|
+
});
|
|
258
|
+
throw new Errors.CLIError(`Could not get an access token for organization ${params.orgId}${result.description ? `: ${result.description}` : "."}`, { exit: 1 });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function saveOrgToken(credentials, org, makeCurrent) {
|
|
262
|
+
const grant = requireUserGrant(credentials);
|
|
263
|
+
const cache = { ...grant.org_tokens };
|
|
264
|
+
if (makeCurrent) {
|
|
265
|
+
if (credentials.org_id && credentials.access_token) cache[credentials.org_id] = {
|
|
266
|
+
access_token: credentials.access_token,
|
|
267
|
+
expires_at: credentials.expires_at,
|
|
268
|
+
oauth_grant_id: credentials.oauth_grant_id,
|
|
269
|
+
org_id: credentials.org_id,
|
|
270
|
+
org_name: credentials.org_name,
|
|
271
|
+
role: grant.org_role ?? null
|
|
272
|
+
};
|
|
273
|
+
delete cache[org.org_id];
|
|
274
|
+
return {
|
|
275
|
+
...credentialsForOrg(credentials, org),
|
|
276
|
+
user_grant: {
|
|
277
|
+
...grant,
|
|
278
|
+
org_role: org.role,
|
|
279
|
+
org_tokens: pruneOrgTokens(cache)
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (org.org_id === credentials.org_id) return {
|
|
284
|
+
...credentialsForOrg(credentials, org),
|
|
285
|
+
user_grant: {
|
|
286
|
+
...grant,
|
|
287
|
+
org_role: org.role
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
cache[org.org_id] = org;
|
|
291
|
+
return {
|
|
292
|
+
...credentials,
|
|
293
|
+
user_grant: {
|
|
294
|
+
...grant,
|
|
295
|
+
org_tokens: pruneOrgTokens(cache)
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function pruneOrgTokens(cache) {
|
|
300
|
+
const entries = Object.entries(cache).sort(([, left], [, right]) => Date.parse(right.expires_at) - Date.parse(left.expires_at));
|
|
301
|
+
return Object.fromEntries(entries.slice(0, MAX_CACHED_ORG_TOKENS));
|
|
302
|
+
}
|
|
303
|
+
/** The organizations the person can switch to. */
|
|
304
|
+
async function fetchCliOrgs(params) {
|
|
305
|
+
const now = params.now ?? Date.now;
|
|
306
|
+
let credentials = params.credentials;
|
|
307
|
+
requireUserGrant(credentials);
|
|
308
|
+
const grant = credentials.user_grant;
|
|
309
|
+
if (expiringSoon(grant.expires_at, now)) credentials = await withLock(params, () => {
|
|
310
|
+
const current = loadCliCredentials(params.configDir) ?? credentials;
|
|
311
|
+
return freshIdentity({
|
|
312
|
+
...params,
|
|
313
|
+
credentials: current,
|
|
314
|
+
now
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
const request = async (token) => (params.fetch ?? fetch)(cliOrgsEndpoint(params.apiBaseUrl), {
|
|
318
|
+
headers: {
|
|
319
|
+
...params.headers ?? {},
|
|
320
|
+
authorization: `Bearer ${token}`
|
|
321
|
+
},
|
|
322
|
+
method: "GET"
|
|
323
|
+
});
|
|
324
|
+
let response = await request(credentials.user_grant.access_token);
|
|
325
|
+
if (response.status === 401) {
|
|
326
|
+
credentials = await withLock(params, () => {
|
|
327
|
+
const current = loadCliCredentials(params.configDir) ?? credentials;
|
|
328
|
+
return freshIdentity({
|
|
329
|
+
...params,
|
|
330
|
+
credentials: current,
|
|
331
|
+
force: true,
|
|
332
|
+
now
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
response = await request(credentials.user_grant.access_token);
|
|
336
|
+
}
|
|
337
|
+
const payload = await response.json().catch(() => null);
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
const error = isRecord(payload) && isRecord(payload.error) ? payload.error : {};
|
|
340
|
+
if (response.status === 404 && error.code === "feature_disabled") throw new Errors.CLIError("This Primitive server does not support switching organizations for CLI logins.", { exit: 1 });
|
|
341
|
+
throw new Errors.CLIError(`Could not list organizations${typeof error.message === "string" ? `: ${error.message}` : ` (HTTP ${response.status}).`}`, { exit: 1 });
|
|
342
|
+
}
|
|
343
|
+
const data = isRecord(payload) ? payload.data : null;
|
|
344
|
+
if (!isRecord(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
|
|
345
|
+
return data;
|
|
346
|
+
}
|
|
347
|
+
/** How long a command waits for another CLI process to finish with the saved login. */
|
|
348
|
+
const LOCK_WAIT_MS = 15e3;
|
|
349
|
+
const LOCK_RETRY_MS = 100;
|
|
350
|
+
function isLockBusy(error) {
|
|
351
|
+
return error instanceof Error && error.message.startsWith("Another Primitive CLI credential operation is already in progress");
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Run `fn` holding the credentials lock. Exchanges happen every few minutes
|
|
355
|
+
* and from parallel commands, so a busy lock is waited for (briefly) rather
|
|
356
|
+
* than refused; callers re-read the saved login once they hold it.
|
|
357
|
+
*/
|
|
358
|
+
async function withLock(params, fn) {
|
|
359
|
+
let release;
|
|
360
|
+
if (!params.credentialsLockHeld) {
|
|
361
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
362
|
+
for (;;) try {
|
|
363
|
+
release = acquireCliCredentialsLock(params.configDir);
|
|
364
|
+
break;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (isLockBusy(error) && Date.now() < deadline) {
|
|
367
|
+
await new Promise((done) => setTimeout(done, LOCK_RETRY_MS));
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (isLockBusy(error)) throw new Errors.CLIError("Another Primitive CLI command is still updating the saved login. Retry in a moment.", { exit: 1 });
|
|
371
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
372
|
+
throw new Errors.CLIError(detail, { exit: 1 });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
return await fn();
|
|
377
|
+
} finally {
|
|
378
|
+
release?.();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Match `selection` (an organization id, or a name) against the person's
|
|
383
|
+
* organizations. Ids pass through without a lookup.
|
|
384
|
+
*/
|
|
385
|
+
function matchOrg(selection, orgs) {
|
|
386
|
+
const wanted = selection.trim();
|
|
387
|
+
const byId = orgs.find((org) => org.id.toLowerCase() === wanted.toLowerCase());
|
|
388
|
+
if (byId) return byId;
|
|
389
|
+
const byName = orgs.filter((org) => (org.name ?? "").trim().toLowerCase() === wanted.toLowerCase());
|
|
390
|
+
if (byName.length === 1) return byName[0];
|
|
391
|
+
if (byName.length > 1) throw new Errors.CLIError(`More than one organization is named ${JSON.stringify(wanted)}. Use its id: ${byName.map((org) => org.id).join(", ")}.`, { exit: 1 });
|
|
392
|
+
throw new Errors.CLIError(`No organization matches ${JSON.stringify(wanted)}. Run \`primitive orgs\` to list yours.`, { exit: 1 });
|
|
393
|
+
}
|
|
394
|
+
/** Resolve an `--org` value to an organization id, listing orgs only for a name. */
|
|
395
|
+
async function resolveOrgSelection(params) {
|
|
396
|
+
const selection = params.selection.trim();
|
|
397
|
+
if (isUuid(selection)) return selection.toLowerCase();
|
|
398
|
+
requireUserGrant(params.credentials);
|
|
399
|
+
return matchOrg(selection, (await fetchCliOrgs(params)).orgs).id;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The org-bound credentials for this command: the current organization, or
|
|
403
|
+
* `orgSelection` (from `--org`) when given. Exchanges when the token for
|
|
404
|
+
* that organization is missing or about to expire.
|
|
405
|
+
*/
|
|
406
|
+
async function ensureUserGrantSession(params) {
|
|
407
|
+
const now = params.now ?? Date.now;
|
|
408
|
+
requireUserGrant(params.credentials);
|
|
409
|
+
const raw = params.orgSelection?.trim() || null;
|
|
410
|
+
const selection = raw === null ? null : await resolveOrgSelection({
|
|
411
|
+
...params,
|
|
412
|
+
credentials: params.credentials,
|
|
413
|
+
now,
|
|
414
|
+
selection: raw
|
|
415
|
+
});
|
|
416
|
+
const fast = usableOrgToken(params.credentials, selection, now);
|
|
417
|
+
if (fast && !params.forceExchange) return fast;
|
|
418
|
+
return withLock(params, async () => {
|
|
419
|
+
const current = loadCliCredentials(params.configDir);
|
|
420
|
+
if (!current) throw new Errors.CLIError(USER_LOGIN_EXPIRED_MESSAGE, { exit: 1 });
|
|
421
|
+
requireUserGrant(current);
|
|
422
|
+
const orgId = selection ?? (current.org_id || null);
|
|
423
|
+
if (!orgId) throw new Errors.CLIError(NO_CURRENT_ORG_MESSAGE, { exit: 1 });
|
|
424
|
+
const saved = usableOrgToken(current, orgId, now);
|
|
425
|
+
if (saved && (!params.forceExchange || params.staleToken !== void 0 && saved.access_token !== params.staleToken)) return saved;
|
|
426
|
+
const { credentials, org } = await exchangeAndSave({
|
|
427
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
428
|
+
configDir: params.configDir,
|
|
429
|
+
credentials: current,
|
|
430
|
+
fetch: params.fetch,
|
|
431
|
+
headers: params.headers,
|
|
432
|
+
makeCurrent: false,
|
|
433
|
+
now,
|
|
434
|
+
orgId
|
|
435
|
+
});
|
|
436
|
+
return credentialsForOrg(credentials, org);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function usableOrgToken(credentials, selection, now) {
|
|
440
|
+
const grant = credentials.user_grant;
|
|
441
|
+
if (!grant) return null;
|
|
442
|
+
const wanted = selection?.toLowerCase() ?? null;
|
|
443
|
+
if (wanted === null || wanted === credentials.org_id.toLowerCase()) {
|
|
444
|
+
if (!credentials.org_id || !credentials.access_token) return null;
|
|
445
|
+
return expiringSoon(credentials.expires_at, now) ? null : credentials;
|
|
446
|
+
}
|
|
447
|
+
const cached = Object.values(grant.org_tokens).find((token) => token.org_id.toLowerCase() === wanted);
|
|
448
|
+
if (!cached || expiringSoon(cached.expires_at, now)) return null;
|
|
449
|
+
return credentialsForOrg(credentials, cached);
|
|
450
|
+
}
|
|
451
|
+
/** `primitive org use`: exchange for `selection` and make it the current organization. */
|
|
452
|
+
async function switchCurrentOrg(params) {
|
|
453
|
+
const now = params.now ?? Date.now;
|
|
454
|
+
return withLock(params, async () => {
|
|
455
|
+
const current = loadCliCredentials(params.configDir);
|
|
456
|
+
if (!current) throw new Errors.CLIError("Not logged in. Run `primitive login` first.", { exit: 1 });
|
|
457
|
+
requireUserGrant(current);
|
|
458
|
+
const orgId = await resolveOrgSelection({
|
|
459
|
+
...params,
|
|
460
|
+
credentials: current,
|
|
461
|
+
credentialsLockHeld: true,
|
|
462
|
+
now
|
|
463
|
+
});
|
|
464
|
+
const latest = loadCliCredentials(params.configDir);
|
|
465
|
+
if (!latest) throw new Errors.CLIError(USER_LOGIN_EXPIRED_MESSAGE, { exit: 1 });
|
|
466
|
+
return exchangeAndSave({
|
|
467
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
468
|
+
configDir: params.configDir,
|
|
469
|
+
credentials: latest,
|
|
470
|
+
fetch: params.fetch,
|
|
471
|
+
headers: params.headers,
|
|
472
|
+
makeCurrent: true,
|
|
473
|
+
now,
|
|
474
|
+
orgId
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Wrap `fetch` so a 401 on a per-org token is answered by one fresh
|
|
480
|
+
* exchange and one retry, as the server asks. If the exchange is refused,
|
|
481
|
+
* the caller sees a 403 carrying the refusal's reason as its error code.
|
|
482
|
+
*/
|
|
483
|
+
function reexchangingFetch(params) {
|
|
484
|
+
const base = (input, init) => (params.base ?? globalThis.fetch)(input, init);
|
|
485
|
+
let pending = null;
|
|
486
|
+
const wrapped = async (input, init) => {
|
|
487
|
+
const request = new Request(input, init);
|
|
488
|
+
const sent = /^Bearer\s+(\S+)$/i.exec(request.headers.get("authorization") ?? "")?.[1];
|
|
489
|
+
const retry = request.clone();
|
|
490
|
+
const response = await base(request);
|
|
491
|
+
if (response.status !== 401 || !sent) return response;
|
|
492
|
+
if (params.token.current === sent) {
|
|
493
|
+
pending ??= params.reexchange(sent).finally(() => {
|
|
494
|
+
pending = null;
|
|
495
|
+
});
|
|
496
|
+
const outcome = await pending;
|
|
497
|
+
if (!outcome.ok) return Response.json({
|
|
498
|
+
success: false,
|
|
499
|
+
error: {
|
|
500
|
+
code: outcome.code,
|
|
501
|
+
message: outcome.message
|
|
502
|
+
}
|
|
503
|
+
}, { status: outcome.status });
|
|
504
|
+
params.token.current = outcome.token;
|
|
505
|
+
}
|
|
506
|
+
retry.headers.set("authorization", `Bearer ${params.token.current}`);
|
|
507
|
+
return base(retry);
|
|
508
|
+
};
|
|
509
|
+
return wrapped;
|
|
510
|
+
}
|
|
511
|
+
/** The reexchange callback for `reexchangingFetch`, for a saved user login. */
|
|
512
|
+
function savedLoginReexchange(params) {
|
|
513
|
+
return async (staleToken) => {
|
|
514
|
+
try {
|
|
515
|
+
const current = loadCliCredentials(params.configDir);
|
|
516
|
+
if (!current?.user_grant) return {
|
|
517
|
+
ok: false,
|
|
518
|
+
status: 401,
|
|
519
|
+
code: "unauthorized",
|
|
520
|
+
message: USER_LOGIN_EXPIRED_MESSAGE
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
ok: true,
|
|
524
|
+
token: (await ensureUserGrantSession({
|
|
525
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
526
|
+
configDir: params.configDir,
|
|
527
|
+
credentials: current,
|
|
528
|
+
fetch: params.fetch,
|
|
529
|
+
forceExchange: true,
|
|
530
|
+
headers: params.headers,
|
|
531
|
+
now: params.now,
|
|
532
|
+
orgSelection: params.orgId,
|
|
533
|
+
staleToken
|
|
534
|
+
})).access_token
|
|
535
|
+
};
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error instanceof CliOrgRefusedError) return {
|
|
538
|
+
ok: false,
|
|
539
|
+
status: 403,
|
|
540
|
+
code: error.reason,
|
|
541
|
+
message: error.message
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
status: 401,
|
|
546
|
+
code: "unauthorized",
|
|
547
|
+
message: error instanceof Error ? error.message : String(error)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/** Sign the person out of every organization by revoking the user login. */
|
|
553
|
+
async function revokeUserLogin(params) {
|
|
554
|
+
const result = await postTokenEndpoint({
|
|
555
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
556
|
+
body: {
|
|
557
|
+
client_id: params.credentials.oauth_client_id,
|
|
558
|
+
token: params.credentials.refresh_token,
|
|
559
|
+
token_type_hint: "refresh_token"
|
|
560
|
+
},
|
|
561
|
+
fetch: params.fetch,
|
|
562
|
+
headers: params.headers,
|
|
563
|
+
path: "/oauth/revoke",
|
|
564
|
+
allowEmptySuccess: true
|
|
565
|
+
});
|
|
566
|
+
if (result.ok) return { ok: true };
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
status: result.status,
|
|
570
|
+
message: result.description ?? result.error ?? `The server answered HTTP ${result.status}.`
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/** Build the saved credentials for a user login from the login poll. */
|
|
574
|
+
function credentialsFromUserLogin(params) {
|
|
575
|
+
const now = params.now ?? Date.now;
|
|
576
|
+
const org = isOrgAccessToken(params.login.org) ? orgTokenFromResponse(params.login.org, now) : null;
|
|
577
|
+
return {
|
|
578
|
+
access_token: org?.access_token ?? "",
|
|
579
|
+
api_base_url: params.apiBaseUrl,
|
|
580
|
+
auth_method: "user_grant",
|
|
581
|
+
created_at: new Date(now()).toISOString(),
|
|
582
|
+
expires_at: org?.expires_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
583
|
+
oauth_client_id: params.login.oauth_client_id,
|
|
584
|
+
oauth_grant_id: org?.oauth_grant_id ?? "",
|
|
585
|
+
org_id: org?.org_id ?? "",
|
|
586
|
+
org_name: org?.org_name ?? null,
|
|
587
|
+
refresh_token: params.login.refresh_token,
|
|
588
|
+
token_type: "Bearer",
|
|
589
|
+
user_grant: {
|
|
590
|
+
access_token: params.login.access_token,
|
|
591
|
+
expires_at: cliAccessTokenExpiresAt(params.login.expires_in, now),
|
|
592
|
+
org_role: org?.role ?? null,
|
|
593
|
+
org_tokens: {},
|
|
594
|
+
requested_grant: params.requestedGrant ?? null,
|
|
595
|
+
user_grant_id: params.login.user_grant_id,
|
|
596
|
+
user_id: params.login.user_id
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// src/oclif/api-client.ts
|
|
9
601
|
const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
|
|
10
602
|
const OAUTH_REFRESH_SKEW_MS = 6e4;
|
|
11
603
|
const SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE = "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
|
|
@@ -99,8 +691,47 @@ function oauthErrorDescription(value) {
|
|
|
99
691
|
const raw = value.error_description;
|
|
100
692
|
return typeof raw === "string" && raw.trim() ? raw : null;
|
|
101
693
|
}
|
|
694
|
+
/** The organization the global `--org` flag (or PRIMITIVE_CLI_ORG) selects, if any. */
|
|
695
|
+
function selectedOrgFromEnv(env = process.env) {
|
|
696
|
+
return env["PRIMITIVE_CLI_ORG"]?.trim() || null;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Refuse `--org` before a command acts with a connected agent profile's own
|
|
700
|
+
* key: the profile belongs to one organization and cannot act in another.
|
|
701
|
+
*/
|
|
702
|
+
function refuseOrgSelectionForProfile(profileName, env = process.env) {
|
|
703
|
+
const selection = selectedOrgFromEnv(env);
|
|
704
|
+
if (!selection) return;
|
|
705
|
+
const who = profileName ? `Connected agent profile ${profileName}` : "A connected agent credential";
|
|
706
|
+
throw new Errors.CLIError(`--org (PRIMITIVE_CLI_ORG=${selection}) selects an organization for a saved \`primitive login\`. ${who} belongs to one organization; drop --org, or unset PRIMITIVE_CLI_ORG.`, { exit: 1 });
|
|
707
|
+
}
|
|
708
|
+
function orgMatches(credentials, selection) {
|
|
709
|
+
const wanted = selection.toLowerCase();
|
|
710
|
+
return credentials.org_id.toLowerCase() === wanted || (credentials.org_name ?? "").trim().toLowerCase() === wanted;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Refuse `--org` for an org-bound login unless it names that login's own
|
|
714
|
+
* organization: an org-bound login cannot act anywhere else.
|
|
715
|
+
*/
|
|
716
|
+
function assertOrgBoundSelection(credentials, selection) {
|
|
717
|
+
if (!selection || orgMatches(credentials, selection)) return;
|
|
718
|
+
const org = credentials.org_name ? `${credentials.org_name} (${credentials.org_id})` : credentials.org_id;
|
|
719
|
+
throw new Errors.CLIError(`This saved login is bound to organization ${org}, so --org ${selection} cannot be used with it. Run \`primitive logout\`, then \`primitive login\`, to sign in as yourself and switch between organizations.`, { exit: 1 });
|
|
720
|
+
}
|
|
102
721
|
async function refreshStoredCliCredentials(params) {
|
|
103
722
|
const now = params.now ?? Date.now;
|
|
723
|
+
const orgSelection = params.orgSelection === void 0 ? selectedOrgFromEnv() : params.orgSelection;
|
|
724
|
+
if (params.credentials.auth_method === "user_grant") return ensureUserGrantSession({
|
|
725
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
726
|
+
configDir: params.configDir,
|
|
727
|
+
credentials: params.credentials,
|
|
728
|
+
credentialsLockHeld: params.credentialsLockHeld,
|
|
729
|
+
fetch: params.fetch,
|
|
730
|
+
headers: params.headers,
|
|
731
|
+
now,
|
|
732
|
+
orgSelection
|
|
733
|
+
});
|
|
734
|
+
assertOrgBoundSelection(params.credentials, orgSelection);
|
|
104
735
|
if (!shouldRefresh(params.credentials, now)) return params.credentials;
|
|
105
736
|
let releaseLock;
|
|
106
737
|
try {
|
|
@@ -112,6 +743,16 @@ async function refreshStoredCliCredentials(params) {
|
|
|
112
743
|
}
|
|
113
744
|
const current = loadCliCredentials(params.configDir);
|
|
114
745
|
if (!current) throw new Errors.CLIError("Saved Primitive CLI OAuth session is no longer available. Run `primitive signin` to authenticate again.", { exit: 1 });
|
|
746
|
+
if (current.auth_method === "user_grant") return ensureUserGrantSession({
|
|
747
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
748
|
+
configDir: params.configDir,
|
|
749
|
+
credentials: current,
|
|
750
|
+
credentialsLockHeld: true,
|
|
751
|
+
fetch: params.fetch,
|
|
752
|
+
headers: params.headers,
|
|
753
|
+
now,
|
|
754
|
+
orgSelection
|
|
755
|
+
});
|
|
115
756
|
if (!shouldRefresh(current, now)) return current;
|
|
116
757
|
const fetchImpl = params.fetch ?? fetch;
|
|
117
758
|
const body = new URLSearchParams({
|
|
@@ -172,6 +813,9 @@ async function createAuthenticatedCliApiClient(params) {
|
|
|
172
813
|
if (hint) process.stderr.write(`${hint}\n`);
|
|
173
814
|
}
|
|
174
815
|
if (params.requireConnectedAgentKey && (auth.source === "stored" || !auth.apiKey?.startsWith("pconn_"))) throw new ConnectedAgentKeyRequiredError("A connected agent credential is required.");
|
|
816
|
+
const orgSelection = selectedOrgFromEnv(params.env ?? process.env);
|
|
817
|
+
if (orgSelection && auth.source !== "stored") throw new Errors.CLIError(`--org (PRIMITIVE_CLI_ORG=${orgSelection}) selects an organization for a saved \`primitive login\`. ${auth.source === "connected-profile" ? "A connected agent profile" : auth.source === "flag-or-env" ? "An API key" : "This command's credential"} belongs to one organization; drop --org, or unset PRIMITIVE_CLI_ORG.`, { exit: 1 });
|
|
818
|
+
let userLoginToken = null;
|
|
175
819
|
if (auth.source === "stored" && auth.credentials) {
|
|
176
820
|
const refreshed = await refreshStoredCliCredentials({
|
|
177
821
|
apiBaseUrl: auth.apiBaseUrl,
|
|
@@ -180,17 +824,33 @@ async function createAuthenticatedCliApiClient(params) {
|
|
|
180
824
|
credentialsLockHeld: params.credentialsLockHeld,
|
|
181
825
|
fetch: params.fetch,
|
|
182
826
|
headers: requestConfig.headers,
|
|
183
|
-
now: params.now
|
|
827
|
+
now: params.now,
|
|
828
|
+
orgSelection
|
|
184
829
|
});
|
|
185
830
|
auth = {
|
|
186
831
|
...auth,
|
|
187
832
|
apiKey: refreshed.access_token,
|
|
188
833
|
credentials: refreshed
|
|
189
834
|
};
|
|
835
|
+
if (refreshed.auth_method === "user_grant") userLoginToken = { current: refreshed.access_token };
|
|
190
836
|
}
|
|
837
|
+
const tokenHolder = userLoginToken;
|
|
191
838
|
return {
|
|
192
839
|
apiClient: new PrimitiveApiClient({
|
|
193
|
-
|
|
840
|
+
...tokenHolder && auth.credentials ? {
|
|
841
|
+
auth: (security) => security.type === "http" && security.scheme === "bearer" ? tokenHolder.current : void 0,
|
|
842
|
+
fetch: reexchangingFetch({
|
|
843
|
+
reexchange: savedLoginReexchange({
|
|
844
|
+
apiBaseUrl: auth.apiBaseUrl,
|
|
845
|
+
configDir: params.configDir,
|
|
846
|
+
fetch: params.fetch,
|
|
847
|
+
headers: requestConfig.headers,
|
|
848
|
+
now: params.now,
|
|
849
|
+
orgId: auth.credentials.org_id
|
|
850
|
+
}),
|
|
851
|
+
token: tokenHolder
|
|
852
|
+
})
|
|
853
|
+
} : { apiKey: auth.apiKey },
|
|
194
854
|
apiBaseUrl: auth.apiBaseUrl,
|
|
195
855
|
headers: auth.connectedAgent ? void 0 : requestConfig.headers
|
|
196
856
|
}),
|
|
@@ -199,8 +859,7 @@ async function createAuthenticatedCliApiClient(params) {
|
|
|
199
859
|
requestConfig
|
|
200
860
|
};
|
|
201
861
|
}
|
|
202
|
-
|
|
203
|
-
//#region src/oclif/attachment-wait.ts
|
|
862
|
+
// src/oclif/attachment-wait.ts
|
|
204
863
|
/** Generated download commands that wait for attachment content by default. */
|
|
205
864
|
const ATTACHMENT_WAIT_OPERATIONS = /* @__PURE__ */ new Set([
|
|
206
865
|
"downloadAttachments",
|
|
@@ -255,8 +914,7 @@ async function withAttachmentWait(call, options) {
|
|
|
255
914
|
}
|
|
256
915
|
return result;
|
|
257
916
|
}
|
|
258
|
-
|
|
259
|
-
//#region src/oclif/endpoint-event-types.ts
|
|
917
|
+
// src/oclif/endpoint-event-types.ts
|
|
260
918
|
const EVENT_TYPES_FLAG_OPERATIONS = /* @__PURE__ */ new Set(["createEndpoint", "updateEndpoint"]);
|
|
261
919
|
const EVENT_TYPES_FLAG_DESCRIPTION = "Event types this endpoint subscribes to, comma separated or repeated (sets rules.event_types). Omitting the subscription means every event type except the opt-in sent_email.* events, which an endpoint receives only when it lists them; sent_email.* expands to all four. Keep email.received in the list if the endpoint also handles inbound mail. On update, the endpoint's other rules are kept.";
|
|
262
920
|
const SENT_EMAIL_WILDCARD = "sent_email.*";
|
|
@@ -328,8 +986,7 @@ function withEventTypes(body, eventTypes, baseRules = {}) {
|
|
|
328
986
|
function bodyHasRules(body) {
|
|
329
987
|
return isPlainObject(body) && body.rules !== void 0;
|
|
330
988
|
}
|
|
331
|
-
|
|
332
|
-
//#region src/oclif/endpoints-test-redirect.ts
|
|
989
|
+
// src/oclif/endpoints-test-redirect.ts
|
|
333
990
|
async function detectFunctionEndpoint(endpointId, listEndpoints) {
|
|
334
991
|
let response;
|
|
335
992
|
try {
|
|
@@ -371,8 +1028,7 @@ async function maybeWriteFunctionEndpointRedirect(inputs) {
|
|
|
371
1028
|
inputs.writeStderr(`${formatFunctionEndpointRedirect(match)}\n`);
|
|
372
1029
|
return match;
|
|
373
1030
|
}
|
|
374
|
-
|
|
375
|
-
//#region src/oclif/flag-suggestions.ts
|
|
1031
|
+
// src/oclif/flag-suggestions.ts
|
|
376
1032
|
/**
|
|
377
1033
|
* "Did you mean" hints for unknown flags. oclif reports an unknown flag as
|
|
378
1034
|
* `Nonexistent flag: --query` and prints the full help; this adds the
|
|
@@ -437,8 +1093,7 @@ function withFlagSuggestion(error, valid) {
|
|
|
437
1093
|
].join("\n");
|
|
438
1094
|
return error;
|
|
439
1095
|
}
|
|
440
|
-
|
|
441
|
-
//#region src/oclif/from-verified-filter.ts
|
|
1096
|
+
// src/oclif/from-verified-filter.ts
|
|
442
1097
|
/**
|
|
443
1098
|
* The `from_verified` filter on GET /emails and GET /emails/search: the
|
|
444
1099
|
* `from_verified=true|false` parameter (`--from-verified`) and the
|
|
@@ -494,8 +1149,7 @@ function assertFromVerifiedRows(rows, expectation) {
|
|
|
494
1149
|
}).length;
|
|
495
1150
|
if (wrong > 0) throw new FromVerifiedFilterUnsupportedError(`${expectation.surface} returned ${wrong} of ${rows.length} email${rows.length === 1 ? "" : "s"} without the requested \`from_verified\` value.`);
|
|
496
1151
|
}
|
|
497
|
-
|
|
498
|
-
//#region src/oclif/idempotent-replay-banner.ts
|
|
1152
|
+
// src/oclif/idempotent-replay-banner.ts
|
|
499
1153
|
/**
|
|
500
1154
|
* If `data` carries `idempotent_replay: true`, write a stderr notice
|
|
501
1155
|
* that nothing new was sent and why. Otherwise no-op.
|
|
@@ -509,7 +1163,7 @@ function isReplay(data) {
|
|
|
509
1163
|
if (data === null || typeof data !== "object") return false;
|
|
510
1164
|
return data.idempotent_replay === true;
|
|
511
1165
|
}
|
|
512
|
-
function nonEmptyString(value) {
|
|
1166
|
+
function nonEmptyString$1(value) {
|
|
513
1167
|
return typeof value === "string" && value ? value : null;
|
|
514
1168
|
}
|
|
515
1169
|
/**
|
|
@@ -529,8 +1183,8 @@ function readReplayDetail(sent, keyOrigin) {
|
|
|
529
1183
|
else return null;
|
|
530
1184
|
return {
|
|
531
1185
|
kind,
|
|
532
|
-
originalId: nonEmptyString(object.original_sent_email_id) ?? nonEmptyString(sent.id),
|
|
533
|
-
originalCreatedAt: nonEmptyString(object.original_created_at)
|
|
1186
|
+
originalId: nonEmptyString$1(object.original_sent_email_id) ?? nonEmptyString$1(sent.id),
|
|
1187
|
+
originalCreatedAt: nonEmptyString$1(object.original_created_at)
|
|
534
1188
|
};
|
|
535
1189
|
}
|
|
536
1190
|
/** "12s ago", "4m ago", "3h ago", "9d ago"; null when the time is unusable. */
|
|
@@ -547,8 +1201,8 @@ function formatReplayAge(createdAt, now) {
|
|
|
547
1201
|
return `${Math.round(hours / 24)}d ago`;
|
|
548
1202
|
}
|
|
549
1203
|
function statusSuffix(sent) {
|
|
550
|
-
const status = nonEmptyString(sent.status);
|
|
551
|
-
const deliveryStatus = nonEmptyString(sent.delivery_status);
|
|
1204
|
+
const status = nonEmptyString$1(sent.status);
|
|
1205
|
+
const deliveryStatus = nonEmptyString$1(sent.delivery_status);
|
|
552
1206
|
const details = [];
|
|
553
1207
|
if (status) details.push(`status ${status}`);
|
|
554
1208
|
if (deliveryStatus && deliveryStatus !== status) details.push(`delivery status ${deliveryStatus}`);
|
|
@@ -564,7 +1218,7 @@ function formatSpecificNotice(sent, detail, options) {
|
|
|
564
1218
|
switch (detail.kind) {
|
|
565
1219
|
case "auto_content": return `Not sent: identical to ${id}${when}${state}.${advise ? ` Pass ${keyHint} to send again.` : ""}`;
|
|
566
1220
|
case "explicit": {
|
|
567
|
-
const key = nonEmptyString(sent.client_idempotency_key);
|
|
1221
|
+
const key = nonEmptyString$1(sent.client_idempotency_key);
|
|
568
1222
|
return `Not sent: ${key ? `idempotency key ${key}` : "this idempotency key"} was already used for ${id}${when}${state}. A key you pass never expires.${advise ? " Use a different key only for a different message." : ""}`;
|
|
569
1223
|
}
|
|
570
1224
|
case "function_trigger": return `Not sent: this Function already sent this message as ${id}${when}${state}, for the same email or event that invoked it. A Function that runs again for one trigger sends once.`;
|
|
@@ -593,18 +1247,66 @@ function formatAlreadySentNotice(sent, options = {}) {
|
|
|
593
1247
|
if (deliveryStatus && deliveryStatus !== status) details.push(`delivery status ${deliveryStatus}`);
|
|
594
1248
|
return `Already sent: this exact message went out earlier${details.length > 0 ? ` (${details.join(", ")})` : ""}. Nothing new was sent.`;
|
|
595
1249
|
}
|
|
596
|
-
|
|
597
|
-
|
|
1250
|
+
// src/oclif/runtime-session.ts
|
|
1251
|
+
/**
|
|
1252
|
+
* The variables each runtime sets to its exact session ID, first match wins.
|
|
1253
|
+
* The loaded Codex thread is the receiving identity; the process session ID
|
|
1254
|
+
* is only a fallback when no thread ID is exposed.
|
|
1255
|
+
*/
|
|
1256
|
+
const RUNTIME_SESSION_VARIABLES = {
|
|
1257
|
+
claude: ["CLAUDE_CODE_SESSION_ID"],
|
|
1258
|
+
codex: ["CODEX_THREAD_ID", "CODEX_SESSION_ID"]
|
|
1259
|
+
};
|
|
1260
|
+
const UUID$3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1261
|
+
/** Each runtime whose environment names a session UUID. Never guessed from history or cwd. */
|
|
1262
|
+
function runtimeSessions(env) {
|
|
1263
|
+
return Object.keys(RUNTIME_SESSION_VARIABLES).flatMap((runtime) => {
|
|
1264
|
+
for (const variable of RUNTIME_SESSION_VARIABLES[runtime]) {
|
|
1265
|
+
const value = env[variable]?.trim().toLowerCase();
|
|
1266
|
+
if (!value) continue;
|
|
1267
|
+
return UUID$3.test(value) ? [{
|
|
1268
|
+
runtime,
|
|
1269
|
+
session: value,
|
|
1270
|
+
variable
|
|
1271
|
+
}] : [];
|
|
1272
|
+
}
|
|
1273
|
+
return [];
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
/** The one session the runtime environment names, if exactly one runtime names one. */
|
|
1277
|
+
function detectRuntimeSession(env = process.env) {
|
|
1278
|
+
const candidates = runtimeSessions(env);
|
|
1279
|
+
const [only] = candidates;
|
|
1280
|
+
if (!only) return { kind: "none" };
|
|
1281
|
+
return candidates.length === 1 ? {
|
|
1282
|
+
kind: "detected",
|
|
1283
|
+
...only
|
|
1284
|
+
} : {
|
|
1285
|
+
kind: "ambiguous",
|
|
1286
|
+
candidates
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* The runtime whose tool environment names this exact session. A session ID
|
|
1291
|
+
* that matches neither runtime's variable, or both, has no detectable runtime.
|
|
1292
|
+
*/
|
|
1293
|
+
function detectAgentRuntime(session, env = process.env) {
|
|
1294
|
+
const wanted = session.trim().toLowerCase();
|
|
1295
|
+
const matches = runtimeSessions(env).filter((candidate) => candidate.session === wanted);
|
|
1296
|
+
return matches.length === 1 ? matches[0]?.runtime ?? null : null;
|
|
1297
|
+
}
|
|
1298
|
+
/** Every variable a runtime may name its session in, for messages. */
|
|
1299
|
+
function runtimeSessionVariableList() {
|
|
1300
|
+
const names = Object.values(RUNTIME_SESSION_VARIABLES).flat();
|
|
1301
|
+
return `${names.slice(0, -1).join(", ")} or ${names.at(-1)}`;
|
|
1302
|
+
}
|
|
1303
|
+
// src/oclif/mail-session.ts
|
|
598
1304
|
/** Runtime-provided identity only. Never guess a session from history or cwd. */
|
|
599
1305
|
function currentMailSessionKey(env = process.env) {
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return value && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(value) ? `${codex ? "codex" : "claude"}:${value.toLowerCase()}` : null;
|
|
605
|
-
}
|
|
606
|
-
//#endregion
|
|
607
|
-
//#region src/oclif/agent-identity-suggestions.ts
|
|
1306
|
+
const detection = detectRuntimeSession(env);
|
|
1307
|
+
return detection.kind === "detected" ? `${detection.runtime}:${detection.session}` : null;
|
|
1308
|
+
}
|
|
1309
|
+
// src/oclif/agent-identity-suggestions.ts
|
|
608
1310
|
/** The connection name enrollment uses when none is given. */
|
|
609
1311
|
const DEFAULT_ENROLL_NAME = "Coding agent";
|
|
610
1312
|
/** npx runs the CLI from its cache; print follow-up commands the same way. */
|
|
@@ -687,8 +1389,7 @@ function identitySuggestions(params) {
|
|
|
687
1389
|
suggestions
|
|
688
1390
|
};
|
|
689
1391
|
}
|
|
690
|
-
|
|
691
|
-
//#region src/oclif/send-outcome.ts
|
|
1392
|
+
// src/oclif/send-outcome.ts
|
|
692
1393
|
const SEND_OUTCOME_EXIT_CODES = {
|
|
693
1394
|
replied: 0,
|
|
694
1395
|
sent: 0,
|
|
@@ -1156,8 +1857,7 @@ function thrownErrorExitCode(error) {
|
|
|
1156
1857
|
const exit = error?.oclif?.exit;
|
|
1157
1858
|
return typeof exit === "number" ? exit : 1;
|
|
1158
1859
|
}
|
|
1159
|
-
|
|
1160
|
-
//#region src/oclif/interaction-actions.ts
|
|
1860
|
+
// src/oclif/interaction-actions.ts
|
|
1161
1861
|
const HINTS = /* @__PURE__ */ new Set([
|
|
1162
1862
|
"status",
|
|
1163
1863
|
"card",
|
|
@@ -1173,7 +1873,6 @@ function record$1(value) {
|
|
|
1173
1873
|
function categoryOf(hint, kind, fyi, candidate) {
|
|
1174
1874
|
if (hint === "pending") return candidate ? "pending" : "ordinary";
|
|
1175
1875
|
if (hint === "card") switch (kind) {
|
|
1176
|
-
case "x402.payment/1": return "payment";
|
|
1177
1876
|
case "primitive.contact/1": return "contact";
|
|
1178
1877
|
case "repeat.tick/1": return "repeat";
|
|
1179
1878
|
case "repeat.stop/1": return "repeat_stopped";
|
|
@@ -1188,7 +1887,6 @@ function replyExpectation(category) {
|
|
|
1188
1887
|
case "fyi":
|
|
1189
1888
|
case "signal":
|
|
1190
1889
|
case "repeat_stopped": return "no_reply";
|
|
1191
|
-
case "payment":
|
|
1192
1890
|
case "contact": return "answer_with_command";
|
|
1193
1891
|
case "repeat": return "repeat";
|
|
1194
1892
|
case "unsupported": return "unsupported";
|
|
@@ -1240,19 +1938,6 @@ function interactionNextActions(interaction, emailId, options = {}) {
|
|
|
1240
1938
|
const id = emailId.toLowerCase();
|
|
1241
1939
|
switch (interaction?.category ?? "ordinary") {
|
|
1242
1940
|
case "ordinary": return [replyAction(bin, id)];
|
|
1243
|
-
case "payment": return [buildFollowUpCommand("inspect_payment", "Show the payment request's terms before paying", [
|
|
1244
|
-
bin,
|
|
1245
|
-
"payments",
|
|
1246
|
-
"challenge-from-email",
|
|
1247
|
-
"--id",
|
|
1248
|
-
id
|
|
1249
|
-
]), buildFollowUpCommand("pay", "Pay the payment request (signs with your wallet key and sends the payment)", [
|
|
1250
|
-
bin,
|
|
1251
|
-
"payments",
|
|
1252
|
-
"pay-email",
|
|
1253
|
-
"--in-reply-to",
|
|
1254
|
-
id
|
|
1255
|
-
])];
|
|
1256
1941
|
case "contact": return [buildFollowUpCommand("accept_contact", "Accept the contact request, if the owner's policy allows it", [
|
|
1257
1942
|
bin,
|
|
1258
1943
|
"contacts",
|
|
@@ -1295,11 +1980,6 @@ function interactionHumanLine(interaction, actions) {
|
|
|
1295
1980
|
case "fyi": return "Informational (fyi): no reply needed, not even another fyi.";
|
|
1296
1981
|
case "signal": return `Status signal (${kind}): no reply needed.`;
|
|
1297
1982
|
case "repeat_stopped": return `Notice that a repeating message stopped (${kind}): no reply needed.`;
|
|
1298
|
-
case "payment": {
|
|
1299
|
-
const inspect = command("inspect_payment");
|
|
1300
|
-
const pay = command("pay");
|
|
1301
|
-
return inspect && pay ? `Payment interaction (${kind}). If it requests payment, review it with ${inspect} and pay with ${pay}. A plain reply does not pay or decline it.` : null;
|
|
1302
|
-
}
|
|
1303
1983
|
case "contact": {
|
|
1304
1984
|
const accept = command("accept_contact");
|
|
1305
1985
|
return accept ? `Contact interaction (${kind}). If it is a contact request, accept it under the owner's policy with ${accept}. A plain reply does not accept it.` : null;
|
|
@@ -1374,12 +2054,6 @@ function replyInteractionWarning(detail, emailId) {
|
|
|
1374
2054
|
const expectation = replyExpectation(interaction.category);
|
|
1375
2055
|
if (expectation === "reply" || expectation === "repeat") return null;
|
|
1376
2056
|
switch (interaction.category) {
|
|
1377
|
-
case "payment": return {
|
|
1378
|
-
...base,
|
|
1379
|
-
code: "interaction_not_answered",
|
|
1380
|
-
message: `Warning: email ${id} is a payment interaction (${label}). A plain reply does not pay or decline it; sending anyway. To pay: ${commandFor("pay")}`,
|
|
1381
|
-
expected
|
|
1382
|
-
};
|
|
1383
2057
|
case "contact": return {
|
|
1384
2058
|
...base,
|
|
1385
2059
|
code: "interaction_not_answered",
|
|
@@ -1413,8 +2087,7 @@ function replyInteractionWarning(detail, emailId) {
|
|
|
1413
2087
|
default: return null;
|
|
1414
2088
|
}
|
|
1415
2089
|
}
|
|
1416
|
-
|
|
1417
|
-
//#region src/oclif/pending-mail.ts
|
|
2090
|
+
// src/oclif/pending-mail.ts
|
|
1418
2091
|
const RELATIONSHIPS = [
|
|
1419
2092
|
"owner",
|
|
1420
2093
|
"member",
|
|
@@ -1771,8 +2444,7 @@ function pendingMailProfiles(configDir, session, emailId) {
|
|
|
1771
2444
|
}
|
|
1772
2445
|
}).sort();
|
|
1773
2446
|
}
|
|
1774
|
-
|
|
1775
|
-
//#region src/oclif/wake-context.ts
|
|
2447
|
+
// src/oclif/wake-context.ts
|
|
1776
2448
|
/**
|
|
1777
2449
|
* Mail from a verified owner, member or connected peer agent is mail the
|
|
1778
2450
|
* agent may act on, so its wake starts with the line that loads the rules
|
|
@@ -2009,8 +2681,7 @@ function formatMailWakeLine(input) {
|
|
|
2009
2681
|
const skillFirst = skillFirstLine(input.relation ?? context?.relationship, input.skillFile);
|
|
2010
2682
|
return `${skillFirst ? `${skillFirst}\n` : ""}Primitive mail arrived: ${input.emailId}${input.recipient}${metadata}. Read with ${wakeReadCommand(input.emailId, input.profileName)}.${interaction} ${wakeAuthority(input.relation)}\n`;
|
|
2011
2683
|
}
|
|
2012
|
-
|
|
2013
|
-
//#region src/oclif/pending-mail-miss.ts
|
|
2684
|
+
// src/oclif/pending-mail-miss.ts
|
|
2014
2685
|
/** At most this many other profiles are named in one hint line. */
|
|
2015
2686
|
const HINT_PROFILE_LIMIT = 3;
|
|
2016
2687
|
function currentProfile(configDir) {
|
|
@@ -2064,7 +2735,7 @@ function savedConnectedProfiles(configDir, exclude) {
|
|
|
2064
2735
|
*/
|
|
2065
2736
|
async function otherProfiles(configDir, session, current) {
|
|
2066
2737
|
if (session) try {
|
|
2067
|
-
const { connectedProfilesForSession } = await import("./machine-session-
|
|
2738
|
+
const { connectedProfilesForSession } = await import("./machine-session-Cp_eQ9t_.js").then((n) => n.s);
|
|
2068
2739
|
return {
|
|
2069
2740
|
inSession: true,
|
|
2070
2741
|
profiles: connectedProfilesForSession(configDir, session, current)
|
|
@@ -2138,8 +2809,7 @@ async function explainPendingMailNotFound(configDir, emailId, write = (line) =>
|
|
|
2138
2809
|
if (outcome.kind === "dropped") write(`Dropped the pending notice for ${id} from profile ${current.profile} after ${outcome.reads} consecutive not_found reads; the email is no longer visible to this profile.\n`);
|
|
2139
2810
|
} catch {}
|
|
2140
2811
|
}
|
|
2141
|
-
|
|
2142
|
-
//#region src/oclif/sent-list-labels.ts
|
|
2812
|
+
// src/oclif/sent-list-labels.ts
|
|
2143
2813
|
/**
|
|
2144
2814
|
* `sent list` under a connected agent profile returns this address's own
|
|
2145
2815
|
* sends and also delivered sends from other addresses in the organization
|
|
@@ -2172,12 +2842,13 @@ function sentListOtherSendersNote(rows, self) {
|
|
|
2172
2842
|
const named = senders.length > 3 ? `${senders.slice(0, 3).join(", ")} and ${senders.length - 3} more` : senders.join(", ");
|
|
2173
2843
|
return `${count} of ${rows.length} rows were sent by another address in this organization (${named}) to ${own}; they are mail ${own} received, not its own sends. Rows with from_address ${own} are this address's sends; pass --from ${own} to list only those.`;
|
|
2174
2844
|
}
|
|
2175
|
-
|
|
2176
|
-
//#region src/oclif/api-command.ts
|
|
2845
|
+
// src/oclif/api-command.ts
|
|
2177
2846
|
const API_ERROR_CODES = {
|
|
2178
2847
|
accessDenied: "access_denied",
|
|
2179
2848
|
authorizationPending: "authorization_pending",
|
|
2180
2849
|
expiredToken: "expired_token",
|
|
2850
|
+
forbidden: "forbidden",
|
|
2851
|
+
grantForbidden: "grant_forbidden",
|
|
2181
2852
|
invalidDeviceCode: "invalid_device_code",
|
|
2182
2853
|
notFound: "not_found",
|
|
2183
2854
|
slowDown: "slow_down",
|
|
@@ -2455,6 +3126,47 @@ function rateLimitHint(response) {
|
|
|
2455
3126
|
const delay = parseRetryAfter(response?.headers.get("retry-after"));
|
|
2456
3127
|
return delay === void 0 ? "Hint: this request was rate limited. Wait before retrying: the API request limits use a 60 second window, and send limits can take longer to reset." : `Hint: this request was rate limited. Retry in ${Math.max(1, Math.ceil(delay / 1e3))} s.`;
|
|
2457
3128
|
}
|
|
3129
|
+
/**
|
|
3130
|
+
* True when the API refused because of the credential's grant or role. The
|
|
3131
|
+
* credential itself was accepted, so this is not an authentication failure.
|
|
3132
|
+
*/
|
|
3133
|
+
function isGrantRefusal(payload) {
|
|
3134
|
+
const code = extractErrorCode(payload);
|
|
3135
|
+
return code === API_ERROR_CODES.grantForbidden || code === API_ERROR_CODES.forbidden;
|
|
3136
|
+
}
|
|
3137
|
+
/** `details` of a refusal, whether the payload is the error or its envelope. */
|
|
3138
|
+
function refusalDetails(payload) {
|
|
3139
|
+
const outer = payload && typeof payload === "object" ? payload : {};
|
|
3140
|
+
const inner = outer.error && typeof outer.error === "object" ? outer.error : {};
|
|
3141
|
+
const details = outer.details ?? inner.details;
|
|
3142
|
+
return details && typeof details === "object" ? details : {};
|
|
3143
|
+
}
|
|
3144
|
+
function nonEmptyString(value) {
|
|
3145
|
+
return typeof value === "string" && value ? value : null;
|
|
3146
|
+
}
|
|
3147
|
+
/**
|
|
3148
|
+
* The hint for a `403 grant_forbidden`: which permission the credential's
|
|
3149
|
+
* grant lacks, and how to check a credential before using it.
|
|
3150
|
+
*/
|
|
3151
|
+
function grantForbiddenHint(payload) {
|
|
3152
|
+
const record = refusalDetails(payload);
|
|
3153
|
+
const permission = nonEmptyString(record.permission);
|
|
3154
|
+
const reason = nonEmptyString(record.reason);
|
|
3155
|
+
const missing = nonEmptyString(record.missing);
|
|
3156
|
+
return `Hint: ${permission ? `This credential's grant does not include ${missing ?? permission}${reason ? ` (${reason})` : ""}.` : "This credential's grant does not allow this operation."} Use a credential whose grant includes it, or ask an owner or admin to widen it.${permission ? ` Check a credential with \`primitive whoami --check ${missing ?? permission}\`.` : ""}`;
|
|
3157
|
+
}
|
|
3158
|
+
/**
|
|
3159
|
+
* The hint for a `403 forbidden` that names a permission, or null when it does
|
|
3160
|
+
* not (a dashboard-only route, say, explains itself in its message). Unlike
|
|
3161
|
+
* `grant_forbidden`, this refusal can come from a route closed to keys and the
|
|
3162
|
+
* CLI altogether, so it never promises that a wider credential opens it.
|
|
3163
|
+
*/
|
|
3164
|
+
function forbiddenHint(payload) {
|
|
3165
|
+
const record = refusalDetails(payload);
|
|
3166
|
+
const permission = nonEmptyString(record.permission);
|
|
3167
|
+
if (!permission || record.reason !== "permission_missing") return null;
|
|
3168
|
+
return `Hint: This operation needs ${permission}, which this credential does not have. Some operations stay closed to keys and the CLI even with it. Check a credential with \`primitive whoami --check ${permission}\`.`;
|
|
3169
|
+
}
|
|
2458
3170
|
function writeErrorWithHints(payload, response) {
|
|
2459
3171
|
process.stderr.write(`${formatErrorPayload(payload)}\n`);
|
|
2460
3172
|
const code = extractErrorCode(payload);
|
|
@@ -2463,6 +3175,15 @@ function writeErrorWithHints(payload, response) {
|
|
|
2463
3175
|
return;
|
|
2464
3176
|
}
|
|
2465
3177
|
if (!code) return;
|
|
3178
|
+
if (code === "grant_forbidden") {
|
|
3179
|
+
process.stderr.write(`${grantForbiddenHint(payload)}\n`);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
if (code === API_ERROR_CODES.forbidden) {
|
|
3183
|
+
const hint = forbiddenHint(payload);
|
|
3184
|
+
if (hint) process.stderr.write(`${hint}\n`);
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
2466
3187
|
if (code in ERROR_CODE_HINTS) {
|
|
2467
3188
|
const hint = ERROR_CODE_HINTS[code];
|
|
2468
3189
|
process.stderr.write(`${hint}\n`);
|
|
@@ -2646,14 +3367,25 @@ function operationJsonDocument(envelope, summary = []) {
|
|
|
2646
3367
|
if (summary.length > 0) document.summary = summary.join("\n");
|
|
2647
3368
|
return document;
|
|
2648
3369
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
3370
|
+
const SETTLED_DNS_RECORD_STATUSES = /* @__PURE__ */ new Set(["found", "legacy"]);
|
|
3371
|
+
/**
|
|
3372
|
+
* The hint for a failed domain verification: the API answers 400
|
|
3373
|
+
* `dns_verification_failed` with every expected record, each with its
|
|
3374
|
+
* `status`, in `error.details.records`. Lists the records that still need
|
|
3375
|
+
* work, then how to finish.
|
|
3376
|
+
*/
|
|
3377
|
+
function domainVerificationFailedHint(payload) {
|
|
3378
|
+
const body = payload !== null && typeof payload === "object" ? payload : {};
|
|
3379
|
+
const records = body.details?.records ?? body.error?.details?.records;
|
|
3380
|
+
const lines = (Array.isArray(records) ? records : []).filter((record) => record !== null && typeof record === "object" && !SETTLED_DNS_RECORD_STATUSES.has(String(record.status))).map((record) => {
|
|
3381
|
+
const name = record.fqdn ?? record.name ?? "?";
|
|
3382
|
+
return ` ${String(record.type ?? "?")} ${String(name)} ${String(record.value ?? "")} (${String(record.status ?? "unknown")})`;
|
|
3383
|
+
});
|
|
3384
|
+
return [
|
|
3385
|
+
"Domain verification is incomplete.",
|
|
3386
|
+
...lines.length > 0 ? ["Records to add or fix:", ...lines] : [],
|
|
3387
|
+
"Add or fix those DNS records, or run `primitive domains zone-file --id <domain-id>` to download the complete zone file, then retry `primitive domains verify --id <domain-id>`."
|
|
3388
|
+
].join("\n");
|
|
2657
3389
|
}
|
|
2658
3390
|
const OPERATION_HINTS = {
|
|
2659
3391
|
addDomain: "Tip: after this returns a domain id, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` when the user wants an importable DNS zone file.",
|
|
@@ -2680,11 +3412,14 @@ const OPERATION_EXTRA_BODY_FLAGS = { addDomain: { "mail-relay": {
|
|
|
2680
3412
|
} } };
|
|
2681
3413
|
const MAIL_RELAY_NOT_ENABLED_MESSAGE = "Mail relay is not enabled for this account, so the domain was not added. Retry without --mail-relay to point the domain's MX at Primitive directly, or ask Primitive to enable mail relay for your account.";
|
|
2682
3414
|
const MAIL_RELAY_UNAVAILABLE_MESSAGE = "No mail relay is available on this API server right now, so the domain was not added. Retry later, or retry without --mail-relay to point the domain's MX at Primitive directly.";
|
|
2683
|
-
const OPERATION_ERROR_HINTS = {
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3415
|
+
const OPERATION_ERROR_HINTS = {
|
|
3416
|
+
verifyDomain: (code, _body, payload) => code === "dns_verification_failed" ? domainVerificationFailedHint(payload) : void 0,
|
|
3417
|
+
addDomain: (code, body) => {
|
|
3418
|
+
if (code === "mail_relay_unavailable") return MAIL_RELAY_UNAVAILABLE_MESSAGE;
|
|
3419
|
+
const mailRelay = body !== null && typeof body === "object" && body.mail_relay === true;
|
|
3420
|
+
if (code === "feature_disabled" && mailRelay) return MAIL_RELAY_NOT_ENABLED_MESSAGE;
|
|
3421
|
+
}
|
|
3422
|
+
};
|
|
2688
3423
|
const OPERATION_SUCCESS_HOOKS = { verifyAgentSignup: ({ envelope, configDir, apiBaseUrl, writeStderr }) => {
|
|
2689
3424
|
const data = envelope?.data;
|
|
2690
3425
|
if (!data?.access_token || !data?.refresh_token) return;
|
|
@@ -2776,7 +3511,7 @@ function createOperationCommand(operation) {
|
|
|
2776
3511
|
if (result.error) {
|
|
2777
3512
|
const errorPayload = extractErrorPayload(result.error);
|
|
2778
3513
|
writeErrorWithHints(errorPayload, result.response);
|
|
2779
|
-
const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body);
|
|
3514
|
+
const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload);
|
|
2780
3515
|
if (operationErrorHint) process.stderr.write(`${operationErrorHint}\n`);
|
|
2781
3516
|
if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
|
|
2782
3517
|
if (automatedSurface && isAutomatedRejectedError(errorPayload)) process.stderr.write(`${automatedRejectedError(automatedSurface).message}\n`);
|
|
@@ -2880,10 +3615,6 @@ function createOperationCommand(operation) {
|
|
|
2880
3615
|
process.stderr.write(`Warning: ${operation.sdkName} succeeded but its post-success hook threw (${detail}). The response below is still valid; act on it manually.\n`);
|
|
2881
3616
|
}
|
|
2882
3617
|
this.log(JSON.stringify(jsonOutput && withEnvelope ? operationJsonDocument(envelope, summary) : operationOutputPayload(envelope, withEnvelope), null, 2));
|
|
2883
|
-
if (isIncompleteDomainVerification(operation, envelope)) {
|
|
2884
|
-
writeIncompleteDomainVerificationHint();
|
|
2885
|
-
process.exitCode = 1;
|
|
2886
|
-
}
|
|
2887
3618
|
});
|
|
2888
3619
|
}
|
|
2889
3620
|
}
|
|
@@ -2900,5 +3631,4 @@ const EMPTY_RESULT_HINTS = {
|
|
|
2900
3631
|
function canonicalizeCliReferences(description) {
|
|
2901
3632
|
return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
|
|
2902
3633
|
}
|
|
2903
|
-
|
|
2904
|
-
export { formatDeletedEarlierSendNotice as $, clearReadPendingMail as A, createCliApiClient as At, wakeInteractionSentence as B, serverRelationship as C, withFlagSuggestion as Ct, wakeRelationship as D, ConnectedAgentKeyRequiredError as Dt, wakeRecipientField as E, withAttachmentWait as Et, interactionHumanLine as F, assertValidIdempotencyKey as G, MAX_SEND_WAIT_TIMEOUT_MS as H, interactionNextActions as I, checkPriorReplies as J, buildFollowUpCommand as K, readEmailInteraction as L, readPendingMail as M, resolveCliApiRequestConfig as Mt, recordPendingMail as N, claimPendingMail as O, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as Ot, removePendingMail as P, followUpCommandPrefix as Q, replyExpectation as R, serverMuted as S, writeIdempotentReplayBannerIfReplay as St, wakeReadCommand as T, ATTACHMENT_WAIT_MAX_MS as Tt, SEND_OUTCOME_HELP as U, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as V, apiResultHttpStatus as W, deriveSendIdempotencyKey as X, classifySendError as Y, excludeSignalReplies as Z, displayAddress as _, generatedAgentName as _t, extractErrorCode as a, reportSendCommandResult as at, readThreadContext as b, currentMailSessionKey as bt, hasControlCharacter as c, sendWaitClampNotice as ct, runWithTiming as d, successfulSendOutcome as dt, formatPriorRepliesCheckSkipped as et, surfaceUnauthorizedHint as f, thrownErrorExitCode as ft, describeWake as g, defaultConnectionName as gt, bareAddress$1 as h, connectNextSteps as ht, createOperationCommand as i, priorRepliesThatWentOut as it, pendingMailPath as j, refreshStoredCliCredentials as jt, clearConsumedPendingMail as k, createAuthenticatedCliApiClient as kt, headerFlagName as l, sentHistoryWindowStart as lt, explainPendingMailNotFound as m, cliInvocation as mt, API_ERROR_CODES as n, formatSendFailureSummary as nt, extractErrorPayload as o, sendIdentityFields as ot, writeErrorWithHints as p, DEFAULT_ENROLL_NAME as pt, buildThrownSendFailureEnvelope as q, TIME_FLAG_DESCRIPTION as r, formatSendRecordFailureSummary as rt, formatErrorPayload as s, sendOutcomeExitCode as st, API_BASE_URL_FLAG_DESCRIPTION as t, formatPriorRepliesWarning as tt, readTextFileFlag as u, serializeErrorPayload as ut, formatMailWakeLine as v, identitySuggestions as vt, skillFirstLine as w, ATTACHMENT_WAIT_FLAG_DESCRIPTION as wt, sentInThread as x, formatAlreadySentNotice as xt, latestOwnOutbound as y, loadSkillLine as yt, replyInteractionWarning as z };
|
|
3634
|
+
export { followUpCommandPrefix as $, clearConsumedPendingMail as A, ConnectedAgentKeyRequiredError as At, replyInteractionWarning as B, fetchCliOrgs as Bt, serverMuted as C, detectRuntimeSession as Ct, wakeRecipientField as D, ATTACHMENT_WAIT_FLAG_DESCRIPTION as Dt, wakeReadCommand as E, withFlagSuggestion as Et, removePendingMail as F, refuseOrgSelectionForProfile as Ft, apiResultHttpStatus as G, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as H, switchCurrentOrg as Ht, interactionHumanLine as I, resolveCliApiRequestConfig as It, buildThrownSendFailureEnvelope as J, assertValidIdempotencyKey as K, interactionNextActions as L, selectedOrgFromEnv as Lt, pendingMailPath as M, createAuthenticatedCliApiClient as Mt, readPendingMail as N, createCliApiClient as Nt, wakeRelationship as O, ATTACHMENT_WAIT_MAX_MS as Ot, recordPendingMail as P, refreshStoredCliCredentials as Pt, excludeSignalReplies as Q, readEmailInteraction as R, CliOrgRefusedError as Rt, sentInThread as S, detectAgentRuntime as St, skillFirstLine as T, formatAlreadySentNotice as Tt, MAX_SEND_WAIT_TIMEOUT_MS as U, wakeInteractionSentence as V, revokeUserLogin as Vt, SEND_OUTCOME_HELP as W, classifySendError as X, checkPriorReplies as Y, deriveSendIdempotencyKey as Z, describeWake as _, defaultConnectionName as _t, extractErrorCode as a, priorRepliesThatWentOut as at, latestOwnOutbound as b, loadSkillLine as bt, hasControlCharacter as c, sendOutcomeExitCode as ct, readTextFileFlag as d, serializeErrorPayload as dt, formatDeletedEarlierSendNotice as et, runWithTiming as f, successfulSendOutcome as ft, bareAddress$1 as g, connectNextSteps as gt, explainPendingMailNotFound as h, cliInvocation as ht, createOperationCommand as i, formatSendRecordFailureSummary as it, clearReadPendingMail as j, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as jt, claimPendingMail as k, withAttachmentWait as kt, headerFlagName as l, sendWaitClampNotice as lt, writeErrorWithHints as m, DEFAULT_ENROLL_NAME as mt, API_ERROR_CODES as n, formatPriorRepliesWarning as nt, extractErrorPayload as o, reportSendCommandResult as ot, surfaceUnauthorizedHint as p, thrownErrorExitCode as pt, buildFollowUpCommand as q, TIME_FLAG_DESCRIPTION as r, formatSendFailureSummary as rt, formatErrorPayload as s, sendIdentityFields as st, API_BASE_URL_FLAG_DESCRIPTION as t, formatPriorRepliesCheckSkipped as tt, isGrantRefusal as u, sentHistoryWindowStart as ut, displayAddress as v, generatedAgentName as vt, serverRelationship as w, runtimeSessionVariableList as wt, readThreadContext as x, currentMailSessionKey as xt, formatMailWakeLine as y, identitySuggestions as yt, replyExpectation as z, credentialsFromUserLogin as zt };
|