@alter-ai/cli 0.1.0 → 0.3.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/README.md +1 -1
- package/dist/cli.js +247 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The fastest interactive sign-in is the browser-dance flow — `alter auth login`
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
alter auth login
|
|
19
|
-
# alter: opening browser at https://
|
|
19
|
+
# alter: opening browser at https://portal.alterauth.com/cli-auth
|
|
20
20
|
# (waiting up to 2 minutes for you to approve)…
|
|
21
21
|
# alter: signed in via browser-dance flow.
|
|
22
22
|
|
package/dist/cli.js
CHANGED
|
@@ -176,7 +176,7 @@ async function maybePrintUpdateBanner(currentVersion, argv2 = process.argv.slice
|
|
|
176
176
|
// package.json
|
|
177
177
|
var package_default = {
|
|
178
178
|
name: "@alter-ai/cli",
|
|
179
|
-
version: "0.
|
|
179
|
+
version: "0.3.0",
|
|
180
180
|
description: "Command-line interface for the Alter Vault dev portal \u2014 scripted dashboard automation.",
|
|
181
181
|
type: "module",
|
|
182
182
|
bin: {
|
|
@@ -440,10 +440,23 @@ var GrantRevokedError = class extends ReAuthRequiredError {
|
|
|
440
440
|
};
|
|
441
441
|
var CredentialRevokedError = class extends ReAuthRequiredError {
|
|
442
442
|
grantId;
|
|
443
|
-
|
|
443
|
+
// Recovery-context fields. Populated by the backend at the raise
|
|
444
|
+
// site in token_service so callers can mint a Connect session for
|
|
445
|
+
// the same (user, provider) without re-fetching the grant. See
|
|
446
|
+
// docs/planning/CONNECT_RECOVERY_CONTEXT.md.
|
|
447
|
+
providerId;
|
|
448
|
+
appUserId;
|
|
449
|
+
// Recovery context is appended AFTER `details` so the existing
|
|
450
|
+
// positional contract `new CredentialRevokedError("msg", "g-1",
|
|
451
|
+
// detailsObj)` keeps compiling AND keeps assigning detailsObj to
|
|
452
|
+
// the `details` slot — only the wire-parsing site uses the new
|
|
453
|
+
// positional slots.
|
|
454
|
+
constructor(message, grantId, details, providerId, appUserId) {
|
|
444
455
|
super(message, details);
|
|
445
456
|
this.name = "CredentialRevokedError";
|
|
446
457
|
this.grantId = grantId;
|
|
458
|
+
this.providerId = providerId;
|
|
459
|
+
this.appUserId = appUserId;
|
|
447
460
|
}
|
|
448
461
|
};
|
|
449
462
|
var GrantDeletedError = class extends ReAuthRequiredError {
|
|
@@ -453,9 +466,19 @@ var GrantDeletedError = class extends ReAuthRequiredError {
|
|
|
453
466
|
}
|
|
454
467
|
};
|
|
455
468
|
var GrantNotFoundError = class extends BackendError {
|
|
456
|
-
|
|
469
|
+
providerId;
|
|
470
|
+
agentId;
|
|
471
|
+
appUserId;
|
|
472
|
+
// Recovery context is appended AFTER `details` so the existing
|
|
473
|
+
// positional contract `new GrantNotFoundError("msg", detailsObj)`
|
|
474
|
+
// keeps compiling AND keeps assigning detailsObj to the `details`
|
|
475
|
+
// slot — only the wire-parsing site uses the new positional slots.
|
|
476
|
+
constructor(message, details, providerId, agentId, appUserId) {
|
|
457
477
|
super(message, details);
|
|
458
478
|
this.name = "GrantNotFoundError";
|
|
479
|
+
this.providerId = providerId;
|
|
480
|
+
this.agentId = agentId;
|
|
481
|
+
this.appUserId = appUserId;
|
|
459
482
|
}
|
|
460
483
|
};
|
|
461
484
|
var AmbiguousGrantError = class extends BackendError {
|
|
@@ -468,23 +491,43 @@ var AmbiguousGrantError = class extends BackendError {
|
|
|
468
491
|
// UUIDs only (no emails / names) — same backend hygiene rule that
|
|
469
492
|
// applies to the rest of the cross-tenant probe surface.
|
|
470
493
|
appUserIds;
|
|
471
|
-
|
|
494
|
+
// Populated for the managed-secret grant-level flavor: one user
|
|
495
|
+
// delegated multiple managed-secret grants sharing the same
|
|
496
|
+
// template to the same agent. The SDK caller picks one via
|
|
497
|
+
// `grantId=` on the next request. UUIDs only.
|
|
498
|
+
grantIds;
|
|
499
|
+
constructor(message, providerId, accountIdentifiers, accountWasProvided, appUserIds, details, grantIds) {
|
|
472
500
|
super(message, details);
|
|
473
501
|
this.name = "AmbiguousGrantError";
|
|
474
502
|
this.providerId = providerId;
|
|
475
503
|
this.accountIdentifiers = accountIdentifiers ?? [];
|
|
476
504
|
this.accountWasProvided = accountWasProvided ?? false;
|
|
477
505
|
this.appUserIds = appUserIds ?? [];
|
|
506
|
+
this.grantIds = grantIds ?? [];
|
|
478
507
|
}
|
|
479
508
|
};
|
|
480
509
|
var NoDelegatedGrantError = class extends BackendError {
|
|
510
|
+
// `providerId` + `agentId` shipped in the pre-recovery PR, so they
|
|
511
|
+
// KEEP their positional slots. `appUserId` is new in this PR and
|
|
512
|
+
// appended AFTER `details` so the existing 4-arg call shape
|
|
513
|
+
// `new NoDelegatedGrantError(msg, providerId, agentId, details)`
|
|
514
|
+
// continues to work — only the wire-parsing site uses the new
|
|
515
|
+
// appUserId slot.
|
|
481
516
|
providerId;
|
|
482
517
|
agentId;
|
|
483
|
-
|
|
518
|
+
// Populated when the resolution applied a user filter. Diagnostic /
|
|
519
|
+
// correlation field only — the recovery helper does NOT auto-bind
|
|
520
|
+
// the session to the user. Pass `userToken=` to
|
|
521
|
+
// `createConnectSessionForError` (or configure `userTokenGetter` on
|
|
522
|
+
// the client) when user binding is required. See
|
|
523
|
+
// docs/planning/CONNECT_RECOVERY_CONTEXT.md.
|
|
524
|
+
appUserId;
|
|
525
|
+
constructor(message, providerId, agentId, details, appUserId) {
|
|
484
526
|
super(message, details);
|
|
485
527
|
this.name = "NoDelegatedGrantError";
|
|
486
528
|
this.providerId = providerId;
|
|
487
529
|
this.agentId = agentId;
|
|
530
|
+
this.appUserId = appUserId;
|
|
488
531
|
}
|
|
489
532
|
};
|
|
490
533
|
var PolicyViolationError = class extends BackendError {
|
|
@@ -2895,7 +2938,7 @@ function _extractAdditionalCredentials(token) {
|
|
|
2895
2938
|
return _additionalCredsStore.get(token);
|
|
2896
2939
|
}
|
|
2897
2940
|
var _fetch;
|
|
2898
|
-
var SDK_VERSION = "0.
|
|
2941
|
+
var SDK_VERSION = "0.15.0";
|
|
2899
2942
|
var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
|
|
2900
2943
|
var HTTP_FORBIDDEN = 403;
|
|
2901
2944
|
var HTTP_NO_CONTENT2 = 204;
|
|
@@ -3506,7 +3549,10 @@ ${effectiveConstraints}`;
|
|
|
3506
3549
|
if (errorCode === "credential_revoked") {
|
|
3507
3550
|
throw new CredentialRevokedError(
|
|
3508
3551
|
errorData.message ?? "Credential has been revoked. User must re-authorize.",
|
|
3509
|
-
errorData.grant_id
|
|
3552
|
+
errorData.grant_id,
|
|
3553
|
+
errorData,
|
|
3554
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3555
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3510
3556
|
);
|
|
3511
3557
|
}
|
|
3512
3558
|
if (errorData.error === "scope_mismatch") {
|
|
@@ -3546,13 +3592,17 @@ ${effectiveConstraints}`;
|
|
|
3546
3592
|
const appUserIds = Array.isArray(errorData.app_user_ids) ? errorData.app_user_ids.filter(
|
|
3547
3593
|
(v) => typeof v === "string"
|
|
3548
3594
|
) : [];
|
|
3595
|
+
const grantIds = Array.isArray(errorData.grant_ids) ? errorData.grant_ids.filter(
|
|
3596
|
+
(v) => typeof v === "string"
|
|
3597
|
+
) : [];
|
|
3549
3598
|
throw new AmbiguousGrantError(
|
|
3550
3599
|
errorData.message ?? "JWT identity resolved to multiple grants. Pass the 'account' parameter to disambiguate.",
|
|
3551
3600
|
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3552
3601
|
accountIdentifiers,
|
|
3553
3602
|
errorData.account_was_provided === true,
|
|
3554
3603
|
appUserIds,
|
|
3555
|
-
errorData
|
|
3604
|
+
errorData,
|
|
3605
|
+
grantIds
|
|
3556
3606
|
);
|
|
3557
3607
|
}
|
|
3558
3608
|
if (errorData.error === "no_delegated_grant") {
|
|
@@ -3560,7 +3610,8 @@ ${effectiveConstraints}`;
|
|
|
3560
3610
|
errorData.message ?? "Agent has no active delegation or managed-secret grant for this provider.",
|
|
3561
3611
|
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3562
3612
|
typeof errorData.agent_id === "string" ? errorData.agent_id : void 0,
|
|
3563
|
-
errorData
|
|
3613
|
+
errorData,
|
|
3614
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3564
3615
|
);
|
|
3565
3616
|
}
|
|
3566
3617
|
throw new BackendError(
|
|
@@ -3577,9 +3628,21 @@ ${effectiveConstraints}`;
|
|
|
3577
3628
|
}
|
|
3578
3629
|
if (response.status === HTTP_NOT_FOUND) {
|
|
3579
3630
|
const errorData = await __VaultClient.#safeParseJson(response);
|
|
3631
|
+
if (errorData.error === "no_delegated_grant") {
|
|
3632
|
+
throw new NoDelegatedGrantError(
|
|
3633
|
+
errorData.message ?? "Agent has no active delegation or managed-secret grant for this provider.",
|
|
3634
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3635
|
+
typeof errorData.agent_id === "string" ? errorData.agent_id : void 0,
|
|
3636
|
+
errorData,
|
|
3637
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3638
|
+
);
|
|
3639
|
+
}
|
|
3580
3640
|
throw new GrantNotFoundError(
|
|
3581
3641
|
errorData.message ?? "OAuth grant not found for the given grant_id",
|
|
3582
|
-
errorData
|
|
3642
|
+
errorData,
|
|
3643
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3644
|
+
typeof errorData.agent_id === "string" ? errorData.agent_id : void 0,
|
|
3645
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3583
3646
|
);
|
|
3584
3647
|
}
|
|
3585
3648
|
if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
|
|
@@ -3595,7 +3658,9 @@ ${effectiveConstraints}`;
|
|
|
3595
3658
|
throw new CredentialRevokedError(
|
|
3596
3659
|
errorData.message ?? "Underlying credential has been revoked. User must re-authorize.",
|
|
3597
3660
|
errorData.grant_id,
|
|
3598
|
-
errorData
|
|
3661
|
+
errorData,
|
|
3662
|
+
typeof errorData.provider_id === "string" ? errorData.provider_id : void 0,
|
|
3663
|
+
typeof errorData.app_user_id === "string" ? errorData.app_user_id : void 0
|
|
3599
3664
|
);
|
|
3600
3665
|
}
|
|
3601
3666
|
throw new BackendError(
|
|
@@ -4787,7 +4852,14 @@ ${effectiveConstraints}`;
|
|
|
4787
4852
|
}
|
|
4788
4853
|
sessionBody.agent = options.agent;
|
|
4789
4854
|
}
|
|
4790
|
-
if (
|
|
4855
|
+
if (options.userToken !== void 0) {
|
|
4856
|
+
if (typeof options.userToken !== "string" || options.userToken.length === 0) {
|
|
4857
|
+
throw new AlterValueError(
|
|
4858
|
+
"userToken must be a non-empty string when provided"
|
|
4859
|
+
);
|
|
4860
|
+
}
|
|
4861
|
+
sessionBody.user_token = options.userToken;
|
|
4862
|
+
} else if (this.#userTokenGetter) {
|
|
4791
4863
|
sessionBody.user_token = await this.#getUserToken();
|
|
4792
4864
|
}
|
|
4793
4865
|
const sessionPath = "/sdk/oauth/connect/session";
|
|
@@ -4951,10 +5023,43 @@ ${effectiveConstraints}`;
|
|
|
4951
5023
|
} else {
|
|
4952
5024
|
console.log(`Open this URL to authorize: ${session.connectUrl}`);
|
|
4953
5025
|
}
|
|
5026
|
+
return await this.pollConnectSession(session.sessionToken, {
|
|
5027
|
+
timeoutMs,
|
|
5028
|
+
pollIntervalMs
|
|
5029
|
+
});
|
|
5030
|
+
}
|
|
5031
|
+
/**
|
|
5032
|
+
* Poll a Connect session to completion.
|
|
5033
|
+
*
|
|
5034
|
+
* Use this when your code minted the Connect session itself (e.g.,
|
|
5035
|
+
* via {@link createConnectSession} for a custom UI flow, or via
|
|
5036
|
+
* {@link createConnectSessionForError} for a recovery flow) and
|
|
5037
|
+
* you need to block until the user finishes the consent screen.
|
|
5038
|
+
* {@link connect} is the all-in-one convenience that mints + opens
|
|
5039
|
+
* the browser + polls; this method is the polling half on its own.
|
|
5040
|
+
*
|
|
5041
|
+
* @param sessionToken - A session token from
|
|
5042
|
+
* {@link createConnectSession} or
|
|
5043
|
+
* {@link createConnectSessionForError}.
|
|
5044
|
+
* @param options - `timeoutMs` (default 300000 = 5 min) and
|
|
5045
|
+
* `pollIntervalMs` (default 2000). Milliseconds per ecosystem
|
|
5046
|
+
* convention; the Python SDK's equivalent uses seconds.
|
|
5047
|
+
* @returns One `ConnectResult` per provider the user completed
|
|
5048
|
+
* within the session (multi-provider Connect sessions yield
|
|
5049
|
+
* multiple results).
|
|
5050
|
+
* @throws ConnectTimeoutError if the session didn't complete within
|
|
5051
|
+
* `timeoutMs`.
|
|
5052
|
+
* @throws ConnectFlowError / ConnectDeniedError / ConnectConfigError
|
|
5053
|
+
* for user denial, session expiry, or unrecognized status.
|
|
5054
|
+
* @throws AlterSDKError if the SDK instance has been closed.
|
|
5055
|
+
*/
|
|
5056
|
+
async pollConnectSession(sessionToken, options) {
|
|
5057
|
+
this.#assertNotClosed();
|
|
5058
|
+
const timeoutMs = options?.timeoutMs ?? 3e5;
|
|
5059
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
|
|
4954
5060
|
const deadline = Date.now() + timeoutMs;
|
|
4955
|
-
while (
|
|
4956
|
-
|
|
4957
|
-
const pollResult = await this.#pollSession(session.sessionToken);
|
|
5061
|
+
while (true) {
|
|
5062
|
+
const pollResult = await this.#pollSession(sessionToken);
|
|
4958
5063
|
const pollStatus = pollResult.status;
|
|
4959
5064
|
if (pollStatus === "completed") {
|
|
4960
5065
|
const grantsData = pollResult.grants ?? [];
|
|
@@ -4997,12 +5102,95 @@ ${effectiveConstraints}`;
|
|
|
4997
5102
|
{ status: pollStatus }
|
|
4998
5103
|
);
|
|
4999
5104
|
}
|
|
5105
|
+
const remaining = deadline - Date.now();
|
|
5106
|
+
if (remaining <= 0) break;
|
|
5107
|
+
const sleepMs = Math.min(pollIntervalMs, remaining);
|
|
5108
|
+
await new Promise((resolve2) => setTimeout(resolve2, sleepMs));
|
|
5000
5109
|
}
|
|
5001
5110
|
throw new ConnectTimeoutError(
|
|
5002
5111
|
`OAuth flow did not complete within ${Math.round(timeoutMs / 1e3)} seconds. The user may not have finished authorizing in the browser.`,
|
|
5003
5112
|
{ timeoutMs }
|
|
5004
5113
|
);
|
|
5005
5114
|
}
|
|
5115
|
+
/**
|
|
5116
|
+
* Mint a recovery Connect session from a typed error.
|
|
5117
|
+
*
|
|
5118
|
+
* Use this in the catch block of an identity-mode or agent-mode
|
|
5119
|
+
* request that failed because the user hasn't authorized the
|
|
5120
|
+
* provider yet ({@link NoDelegatedGrantError}), or because the
|
|
5121
|
+
* underlying credential is permanently broken
|
|
5122
|
+
* ({@link CredentialRevokedError}), or because the resolved grant
|
|
5123
|
+
* doesn't exist ({@link GrantNotFoundError} with identity-mode
|
|
5124
|
+
* context).
|
|
5125
|
+
*
|
|
5126
|
+
* What the method reuses from the error:
|
|
5127
|
+
* - `providerId` → threaded into `allowedProviders=[...]`. Required;
|
|
5128
|
+
* missing context throws `AlterValueError`.
|
|
5129
|
+
* - `agentId` → threaded into `agent=` so the recovery session
|
|
5130
|
+
* re-binds the same delegation target. Required for
|
|
5131
|
+
* `NoDelegatedGrantError` recovery; absent on
|
|
5132
|
+
* `GrantNotFoundError` / `CredentialRevokedError` for user-direct
|
|
5133
|
+
* grants, in which case the call falls through to non-delegated
|
|
5134
|
+
* recovery.
|
|
5135
|
+
*
|
|
5136
|
+
* What the method does NOT reuse from the error:
|
|
5137
|
+
* - `appUserId` is exposed on the typed error for caller use
|
|
5138
|
+
* (logging, audit correlation, deciding which user to re-prompt)
|
|
5139
|
+
* but is NOT threaded into the recovery session.
|
|
5140
|
+
* `createConnectSession` binds the session via `userToken` (JWT),
|
|
5141
|
+
* not `appUserId`. To bind the session, pass `userToken=`
|
|
5142
|
+
* explicitly or configure `userTokenGetter` on the SDK client.
|
|
5143
|
+
*
|
|
5144
|
+
* @example
|
|
5145
|
+
* ```ts
|
|
5146
|
+
* try {
|
|
5147
|
+
* await vault.request(HttpMethod.GET, "https://...", { provider: "<provider-id>" });
|
|
5148
|
+
* } catch (e) {
|
|
5149
|
+
* if (e instanceof NoDelegatedGrantError) {
|
|
5150
|
+
* const session = await vault.createConnectSessionForError(e, {
|
|
5151
|
+
* allowedOrigin: "https://app.example.com",
|
|
5152
|
+
* });
|
|
5153
|
+
* redirectUser(session.connectUrl);
|
|
5154
|
+
* const results = await vault.pollConnectSession(session.sessionToken);
|
|
5155
|
+
* // Retry with results[0].grantId
|
|
5156
|
+
* }
|
|
5157
|
+
* }
|
|
5158
|
+
* ```
|
|
5159
|
+
*
|
|
5160
|
+
* @param error - The typed exception. Must carry `providerId`; if
|
|
5161
|
+
* `undefined` (direct-mode 404 from a stale grant_id), recovery
|
|
5162
|
+
* isn't derivable and this method throws `AlterValueError`
|
|
5163
|
+
* rather than guessing.
|
|
5164
|
+
* @param options - Standard `createConnectSession` options. Supplied
|
|
5165
|
+
* per call so the recovery session matches the deployment
|
|
5166
|
+
* shape (popup, mobile redirect, headless). The convenience
|
|
5167
|
+
* method doesn't infer these from the error.
|
|
5168
|
+
* @throws AlterValueError if `error.providerId` is `undefined`.
|
|
5169
|
+
*/
|
|
5170
|
+
async createConnectSessionForError(error, options) {
|
|
5171
|
+
const providerId = error.providerId;
|
|
5172
|
+
if (providerId === void 0) {
|
|
5173
|
+
throw new AlterValueError(
|
|
5174
|
+
"Cannot mint a recovery Connect session: the typed error has no providerId context. This usually means the original call used direct grantId mode and the grantId was stale \u2014 there's no (user, provider) tuple to recover. Catch GrantNotFoundError separately and call createConnectSession() directly with the providers the user should re-authorize."
|
|
5175
|
+
);
|
|
5176
|
+
}
|
|
5177
|
+
const agentId = error.agentId;
|
|
5178
|
+
if (error instanceof NoDelegatedGrantError && agentId === void 0) {
|
|
5179
|
+
throw new AlterValueError(
|
|
5180
|
+
"Cannot mint a delegated recovery Connect session: the typed NoDelegatedGrantError has no agentId context. The backend may be on a version predating the recovery-context fields, or the wire payload was malformed. Upgrade the backend, or catch this and call createConnectSession() explicitly with the right agent=."
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
5183
|
+
return await this.createConnectSession({
|
|
5184
|
+
allowedProviders: [providerId],
|
|
5185
|
+
allowedOrigin: options?.allowedOrigin,
|
|
5186
|
+
returnUrl: options?.returnUrl,
|
|
5187
|
+
metadata: options?.metadata,
|
|
5188
|
+
grantPolicy: options?.grantPolicy,
|
|
5189
|
+
requiredScopes: options?.requiredScopes,
|
|
5190
|
+
agent: agentId,
|
|
5191
|
+
userToken: options?.userToken
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5006
5194
|
/**
|
|
5007
5195
|
* Trigger IDP login for end user via browser.
|
|
5008
5196
|
*
|
|
@@ -6539,8 +6727,13 @@ var _keytarCache;
|
|
|
6539
6727
|
async function loadKeytar() {
|
|
6540
6728
|
if (_keytarCache !== void 0) return _keytarCache;
|
|
6541
6729
|
try {
|
|
6542
|
-
const
|
|
6543
|
-
|
|
6730
|
+
const raw = await import("keytar");
|
|
6731
|
+
const candidate = raw.default ?? raw;
|
|
6732
|
+
if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
|
|
6733
|
+
_keytarCache = candidate;
|
|
6734
|
+
} else {
|
|
6735
|
+
_keytarCache = null;
|
|
6736
|
+
}
|
|
6544
6737
|
} catch {
|
|
6545
6738
|
_keytarCache = null;
|
|
6546
6739
|
}
|
|
@@ -6548,7 +6741,7 @@ async function loadKeytar() {
|
|
|
6548
6741
|
}
|
|
6549
6742
|
function printPlaintextFallbackWarning() {
|
|
6550
6743
|
process.stderr.write(
|
|
6551
|
-
"alter: WARNING \u2014 saving the PAT to a plaintext file (~/.config/alter/auth.toml).\n The OS keychain (keytar) is not
|
|
6744
|
+
"alter: WARNING \u2014 saving the PAT to a plaintext file (~/.config/alter/auth.toml, mode 0600).\n The OS keychain (keytar) is not usable on this host. Common causes:\n - keytar's native module failed to install (Linux: install libsecret-1-dev +\n gnome-keyring; Windows: install VS Build Tools; macOS: usually pre-installed).\n - The native module is present but its API surface doesn't match what the CLI\n expects (file a bug against @alter-ai/cli with your node + npm versions).\n If the cleartext fallback is unacceptable on this host, revoke the PAT from the\n dashboard's `Personal Access Tokens` page after use.\n"
|
|
6552
6745
|
);
|
|
6553
6746
|
}
|
|
6554
6747
|
async function loadStoredAuth2() {
|
|
@@ -6642,11 +6835,11 @@ async function clearStoredAuth2() {
|
|
|
6642
6835
|
|
|
6643
6836
|
// src/portal-client.ts
|
|
6644
6837
|
import { platform, release } from "os";
|
|
6645
|
-
var DEFAULT_BASE_URL = "https://
|
|
6838
|
+
var DEFAULT_BASE_URL = "https://backend.alterauth.com";
|
|
6646
6839
|
var PAT_API_PREFIX = "/api/v1/dev-portal";
|
|
6647
6840
|
var HTTP_ERROR_THRESHOLD = 400;
|
|
6648
6841
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6649
|
-
var CLI_VERSION = "0.
|
|
6842
|
+
var CLI_VERSION = "0.3.0";
|
|
6650
6843
|
var USER_AGENT = buildUserAgent();
|
|
6651
6844
|
function buildUserAgent() {
|
|
6652
6845
|
let osTag = "";
|
|
@@ -9383,7 +9576,7 @@ var DEFAULT_SCOPES = [
|
|
|
9383
9576
|
"dashboard_secrets:write"
|
|
9384
9577
|
];
|
|
9385
9578
|
function deriveDashboardUrl(baseUrl) {
|
|
9386
|
-
return baseUrl.replace(/^https:\/\/
|
|
9579
|
+
return baseUrl.replace(/^https:\/\/backend\./, "https://portal.");
|
|
9387
9580
|
}
|
|
9388
9581
|
function defaultOpenBrowser(url) {
|
|
9389
9582
|
let command;
|
|
@@ -9500,7 +9693,7 @@ async function runBrowserDance(options) {
|
|
|
9500
9693
|
server.close();
|
|
9501
9694
|
reject(
|
|
9502
9695
|
new Error(
|
|
9503
|
-
|
|
9696
|
+
`browser-dance callback came from an unexpected origin \u2014 refusing to accept. Expected origin ${JSON.stringify(dashboardOrigin)}; received Origin: ${JSON.stringify(origin || "(none)")}, Referer: ${JSON.stringify(referer || "(none)")}. If your dashboard isn't at the regex-derived host, re-run with --dashboard-url <https-url> pointing at the actual dashboard origin.`
|
|
9504
9697
|
)
|
|
9505
9698
|
);
|
|
9506
9699
|
return;
|
|
@@ -9634,8 +9827,21 @@ async function runBrowserDance(options) {
|
|
|
9634
9827
|
pat,
|
|
9635
9828
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
9636
9829
|
});
|
|
9830
|
+
const smokeTest = options.smokeTest ?? defaultSmokeTest;
|
|
9831
|
+
await smokeTest(pat, options.baseUrl);
|
|
9637
9832
|
process.stdout.write("alter: signed in via browser-dance flow.\n");
|
|
9638
9833
|
}
|
|
9834
|
+
async function defaultSmokeTest(pat, baseUrl) {
|
|
9835
|
+
const client = new DashboardClient({
|
|
9836
|
+
pat,
|
|
9837
|
+
...baseUrl ? { baseUrl } : {}
|
|
9838
|
+
});
|
|
9839
|
+
try {
|
|
9840
|
+
await client.pats.whoami();
|
|
9841
|
+
} finally {
|
|
9842
|
+
await client.close();
|
|
9843
|
+
}
|
|
9844
|
+
}
|
|
9639
9845
|
|
|
9640
9846
|
// src/scope-catalog.ts
|
|
9641
9847
|
var DASHBOARD_RESOURCE_VERBS = {
|
|
@@ -9849,7 +10055,7 @@ async function loginCommand(options) {
|
|
|
9849
10055
|
let resolvedBaseUrl2;
|
|
9850
10056
|
try {
|
|
9851
10057
|
const envBaseUrl = process.env.ALTER_BASE_URL?.trim() || null;
|
|
9852
|
-
resolvedBaseUrl2 = options.baseUrl ? validateBaseUrl(options.baseUrl) : envBaseUrl ? validateBaseUrl(envBaseUrl) : "https://
|
|
10058
|
+
resolvedBaseUrl2 = options.baseUrl ? validateBaseUrl(options.baseUrl) : envBaseUrl ? validateBaseUrl(envBaseUrl) : "https://backend.alterauth.com";
|
|
9853
10059
|
} catch (e) {
|
|
9854
10060
|
err(e instanceof Error ? e.message : String(e));
|
|
9855
10061
|
process.exit(EXIT_USAGE);
|
|
@@ -9875,10 +10081,8 @@ async function loginCommand(options) {
|
|
|
9875
10081
|
process.exit(EXIT_USAGE);
|
|
9876
10082
|
}
|
|
9877
10083
|
}
|
|
9878
|
-
let bindOverride =
|
|
9879
|
-
if (options.bindIp ===
|
|
9880
|
-
bindOverride = { bind: "none" };
|
|
9881
|
-
} else if (typeof options.bindIp === "string") {
|
|
10084
|
+
let bindOverride = { bind: "none" };
|
|
10085
|
+
if (typeof options.bindIp === "string") {
|
|
9882
10086
|
let normalisedCidr;
|
|
9883
10087
|
try {
|
|
9884
10088
|
normalisedCidr = validateBindIpFlag(options.bindIp);
|
|
@@ -9897,6 +10101,19 @@ async function loginCommand(options) {
|
|
|
9897
10101
|
});
|
|
9898
10102
|
return;
|
|
9899
10103
|
} catch (e) {
|
|
10104
|
+
if (e instanceof PortalBackendError) {
|
|
10105
|
+
const isAuthTier = e.statusCode === 401 || e.statusCode === 403;
|
|
10106
|
+
if (isAuthTier) {
|
|
10107
|
+
err(`PAT was minted but cannot authenticate: ${e.message}`);
|
|
10108
|
+
} else {
|
|
10109
|
+
err(`PAT was minted but the backend rejected the smoke test: ${e.message}`);
|
|
10110
|
+
}
|
|
10111
|
+
process.exit(exitCodeForHttpStatus(e.statusCode));
|
|
10112
|
+
}
|
|
10113
|
+
if (e instanceof NetworkError) {
|
|
10114
|
+
err(`PAT was minted but the backend is unreachable: ${e.message}. Check connectivity and retry.`);
|
|
10115
|
+
process.exit(EXIT_ERROR);
|
|
10116
|
+
}
|
|
9900
10117
|
err(e instanceof Error ? e.message : String(e));
|
|
9901
10118
|
process.exit(EXIT_ERROR);
|
|
9902
10119
|
}
|
|
@@ -10061,16 +10278,16 @@ function buildAuthCommand() {
|
|
|
10061
10278
|
"Override the backend URL (must be https://). Defaults to ALTER_BASE_URL or production."
|
|
10062
10279
|
).option(
|
|
10063
10280
|
"--dashboard-url <url>",
|
|
10064
|
-
"Override the dashboard URL used by the browser-dance flow. Defaults to the backend URL with the leftmost `
|
|
10281
|
+
"Override the dashboard URL used by the browser-dance flow. Defaults to the backend URL with the leftmost `backend.` subdomain swapped for `portal.`. Use this when the dashboard isn't at the regex-derivable host (e.g. staging / self-hosted setups). Must use https://."
|
|
10065
10282
|
).option(
|
|
10066
10283
|
"--scopes <list>",
|
|
10067
10284
|
"Comma-separated scope list to request on the browser-dance consent page (e.g. 'dashboard_apps:read,dashboard_keys:write'). Defaults to the full read+write set across the CLI surface \u2014 enough for every non-destructive day-to-day command. Opt in to admin / delete tiers explicitly: 'dashboard_keys:admin' for runtime-key admin ops, 'dashboard_apps:delete' for cascade-delete an app (note: dashboard_apps does NOT support :admin \u2014 the Destructive-Action Policy separates :delete as its own verb). Each entry is validated client-side against the dashboard scope catalog \u2014 typos fail fast. Mutually exclusive with --token / --token-file / --token-stdin \u2014 pre-minted PATs have their scope set baked at mint time on the dashboard's Settings page; combining the flags is a usage error."
|
|
10068
10285
|
).option(
|
|
10069
10286
|
"--no-bind-ip",
|
|
10070
|
-
"
|
|
10287
|
+
"Explicit opt-out of IP binding. This is now the DEFAULT for the browser-dance flow, so the flag is a no-op alias retained for scripts that pass it explicitly. The resulting PAT works from any source IP. Browser-dance flow only."
|
|
10071
10288
|
).option(
|
|
10072
10289
|
"--bind-ip <cidr>",
|
|
10073
|
-
"Mint the PAT bound to the operator-supplied IP / CIDR (e.g. '203.0.113.5' for a single host, '203.0.113.0/24' for a range, or an IPv6 equivalent). The
|
|
10290
|
+
"Mint the PAT bound to the operator-supplied IP / CIDR (e.g. '203.0.113.5' for a single host, '203.0.113.0/24' for a range, or an IPv6 equivalent). The supplied range becomes the entire allowlist on the minted PAT. Use this when the egress IP is known and stable \u2014 e.g. minting a token for a CI runner whose outbound CIDR you control. Interactive developer-laptop logins should leave this unset (default is now unbound; see --no-bind-ip). If both --bind-ip and --no-bind-ip appear, the one that appears LAST on the command line takes effect (Commander last-wins). Browser-dance flow only."
|
|
10074
10291
|
).action(async (options) => {
|
|
10075
10292
|
await loginCommand(options);
|
|
10076
10293
|
});
|