@capxul/cli 4.20.0-beta.3 → 4.20.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.mjs +1379 -256
- package/dist/main.mjs.map +1 -1
- package/package.json +4 -3
package/dist/main.mjs
CHANGED
|
@@ -2562,18 +2562,9 @@ const USDX_ASSET = {
|
|
|
2562
2562
|
tokenAddress: USDX_ADDRESS_BASE_SEPOLIA,
|
|
2563
2563
|
deploymentStartBlock: USDX_DEPLOYMENT_START_BLOCK,
|
|
2564
2564
|
decimals: 6,
|
|
2565
|
-
source: "
|
|
2565
|
+
source: "legacy"
|
|
2566
2566
|
};
|
|
2567
|
-
|
|
2568
|
-
* The settlement tier: rows the exact-transfer path and the Movement scanner
|
|
2569
|
-
* admit, each carrying the legacy `currency` projection those readers expect.
|
|
2570
|
-
* Adding an owned test asset here would put candidate liquidity on the
|
|
2571
|
-
* money-write path, so M03's rows go to the candidate tier below instead.
|
|
2572
|
-
*/
|
|
2573
|
-
const CONFIGURED_MONEY_ASSETS = [{
|
|
2574
|
-
...USDX_ASSET,
|
|
2575
|
-
currency: USDX_ASSET.peg
|
|
2576
|
-
}];
|
|
2567
|
+
({ ...USDX_ASSET }), USDX_ASSET.peg;
|
|
2577
2568
|
/**
|
|
2578
2569
|
* The candidate tier: five owned test assets, every field read back from Base
|
|
2579
2570
|
* Sepolia after the deployment that created it (chain 84532, blocks
|
|
@@ -2645,12 +2636,25 @@ const SYNTHETIC_TEST_ASSETS = [
|
|
|
2645
2636
|
];
|
|
2646
2637
|
toChainId(84532);
|
|
2647
2638
|
/**
|
|
2639
|
+
* The settlement tier: the owned test assets admitted to the exact-transfer
|
|
2640
|
+
* path and to the treasury reads. USDC, USDT and WETH are equal assets. There
|
|
2641
|
+
* is no default token and no successor to USDX; USDX survives only in
|
|
2642
|
+
* `LEGACY_MONEY_ASSETS` and is not a current treasury row.
|
|
2643
|
+
*/
|
|
2644
|
+
const SETTLEMENT_ASSET_SYMBOLS = [
|
|
2645
|
+
"USDC",
|
|
2646
|
+
"USDT",
|
|
2647
|
+
"WETH"
|
|
2648
|
+
];
|
|
2649
|
+
const CONFIGURED_MONEY_ASSETS = SYNTHETIC_TEST_ASSETS.filter((asset) => SETTLEMENT_ASSET_SYMBOLS.includes(asset.symbol));
|
|
2650
|
+
/**
|
|
2648
2651
|
* Every known asset in ONE identity space. The tiers above declare admission;
|
|
2649
2652
|
* this is the only list an identity resolves against, so an asset is never
|
|
2650
2653
|
* two different things depending on which list a caller happened to read.
|
|
2651
2654
|
*/
|
|
2652
|
-
const MONEY_ASSET_REGISTRY = [
|
|
2655
|
+
const MONEY_ASSET_REGISTRY = [USDX_ASSET, ...SYNTHETIC_TEST_ASSETS];
|
|
2653
2656
|
CONFIGURED_MONEY_ASSETS.map((asset) => asset.tokenAddress.toLowerCase());
|
|
2657
|
+
USDX_ADDRESS_BASE_SEPOLIA.toLowerCase();
|
|
2654
2658
|
/** The registry row for `assetId` across every tier, or `null`. */
|
|
2655
2659
|
function configuredMoneyAssetById(assetId) {
|
|
2656
2660
|
const canonical = toAssetId(assetId);
|
|
@@ -5208,6 +5212,175 @@ new Map([
|
|
|
5208
5212
|
["setAllowance(bytes32,uint128,uint128,uint128,uint64,uint64)", "SetAllowance(bytes32,uint128,uint128,uint128,uint64,uint64)"]
|
|
5209
5213
|
].map(([functionSignature, eventSignature]) => [toFunctionSelector(functionSignature), toEventSelector(eventSignature)]));
|
|
5210
5214
|
//#endregion
|
|
5215
|
+
//#region ../../packages/sdk/src/domain/money/asset-amount.ts
|
|
5216
|
+
/** Convert raw units without rounding or changing asset identity. */
|
|
5217
|
+
function assetAmountFromRaw(raw, asset) {
|
|
5218
|
+
validateMetadata(asset);
|
|
5219
|
+
return {
|
|
5220
|
+
asset: asset.assetId,
|
|
5221
|
+
value: formatUnits(decimalToRaw(toWeiAmount(raw), 0), asset.decimals)
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
5224
|
+
/** Exact scaling. This rejects excess precision instead of rounding it. */
|
|
5225
|
+
function toRaw(amount, asset) {
|
|
5226
|
+
validateMetadata(asset);
|
|
5227
|
+
if (amount.asset !== asset.assetId) throw Errors.invalidInput("amount.asset", "must match the selected asset on its chain");
|
|
5228
|
+
return decimalToRaw(amount.value, asset.decimals);
|
|
5229
|
+
}
|
|
5230
|
+
function requireAsset(id) {
|
|
5231
|
+
const asset = configuredMoneyAssetById(id);
|
|
5232
|
+
if (asset === null) throw Errors.invalidInput("amount.asset", "is not a registered asset");
|
|
5233
|
+
return asset;
|
|
5234
|
+
}
|
|
5235
|
+
function decimalToRaw(value, decimals) {
|
|
5236
|
+
validatePrecision(decimals);
|
|
5237
|
+
if (!/^\d+(?:\.\d+)?$/.test(value) || value.trim() !== value || value.length > 512) throw Errors.invalidInput("amount.value", "must be a non-negative decimal string of at most 512 characters");
|
|
5238
|
+
const [integer = "0", fraction = ""] = value.split(".");
|
|
5239
|
+
if (fraction.length > decimals) throw Errors.invalidInput("amount.value", "fraction exceeds asset precision");
|
|
5240
|
+
return BigInt(integer) * 10n ** BigInt(decimals) + BigInt(fraction.padEnd(decimals, "0") || "0");
|
|
5241
|
+
}
|
|
5242
|
+
function validatePrecision(decimals) {
|
|
5243
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255) throw Errors.invalidInput("decimals", "must be an integer from 0 to 255");
|
|
5244
|
+
}
|
|
5245
|
+
function validateMetadata(asset) {
|
|
5246
|
+
validatePrecision(asset.decimals);
|
|
5247
|
+
if (asset.assetId !== assetIdFor(asset.chainId, asset.tokenAddress)) throw Errors.invalidInput("amount.asset", "metadata must match its chain and token");
|
|
5248
|
+
}
|
|
5249
|
+
//#endregion
|
|
5250
|
+
//#region ../../packages/sdk/src/domain/money/valuation.ts
|
|
5251
|
+
const PEG_RATES = (source, into) => (typeof source === "string" ? source : source.peg) === into ? {
|
|
5252
|
+
value: "1",
|
|
5253
|
+
decimals: 0
|
|
5254
|
+
} : null;
|
|
5255
|
+
/**
|
|
5256
|
+
* The oracle the money READS price with: the account headline, the org
|
|
5257
|
+
* treasury headline, and every later total over the same seam (D6).
|
|
5258
|
+
*
|
|
5259
|
+
* It is `PEG_RATES` today because no price feed exists yet, and a peg is a
|
|
5260
|
+
* relationship, not a market price — so an unpegged asset such as WETH reads
|
|
5261
|
+
* as unrated rather than as a fabricated number. This binding is the ONE place
|
|
5262
|
+
* a real feed arrives; no call site changes when it does.
|
|
5263
|
+
*/
|
|
5264
|
+
const MONEY_READ_RATES = PEG_RATES;
|
|
5265
|
+
/** Sum exact fixed-point values. Missing rates never contribute zero. */
|
|
5266
|
+
function valueIn(amounts, displayCurrency, rates = PEG_RATES) {
|
|
5267
|
+
const currency = toCurrencyCode(displayCurrency);
|
|
5268
|
+
const counted = [];
|
|
5269
|
+
const unrated = [];
|
|
5270
|
+
let sum = 0n;
|
|
5271
|
+
let scale = 0;
|
|
5272
|
+
for (const amount of amounts) {
|
|
5273
|
+
let source;
|
|
5274
|
+
let raw;
|
|
5275
|
+
let decimals;
|
|
5276
|
+
if ("asset" in amount) {
|
|
5277
|
+
source = requireAsset(amount.asset);
|
|
5278
|
+
raw = toRaw(amount, source);
|
|
5279
|
+
decimals = source.decimals;
|
|
5280
|
+
} else {
|
|
5281
|
+
source = toCurrencyCode(amount.currency);
|
|
5282
|
+
raw = decimalToRaw(amount.value, amount.decimals);
|
|
5283
|
+
decimals = amount.decimals;
|
|
5284
|
+
}
|
|
5285
|
+
const rate = rates(source, currency);
|
|
5286
|
+
if (rate === null) {
|
|
5287
|
+
unrated.push(amount);
|
|
5288
|
+
continue;
|
|
5289
|
+
}
|
|
5290
|
+
validatePrecision(rate.decimals);
|
|
5291
|
+
if (!/^\d+$/.test(rate.value) || rate.value.trim() !== rate.value || rate.value.length > 256 || BigInt(rate.value) <= 0n) throw Errors.invalidInput("rate.value", "must be a positive fixed-point integer");
|
|
5292
|
+
const termScale = decimals + rate.decimals;
|
|
5293
|
+
const nextScale = Math.max(scale, termScale);
|
|
5294
|
+
sum = sum * 10n ** BigInt(nextScale - scale) + raw * BigInt(rate.value) * 10n ** BigInt(nextScale - termScale);
|
|
5295
|
+
scale = nextScale;
|
|
5296
|
+
counted.push(amount);
|
|
5297
|
+
}
|
|
5298
|
+
return {
|
|
5299
|
+
total: counted.length === 0 ? null : {
|
|
5300
|
+
currency,
|
|
5301
|
+
value: formatUnits(sum, scale),
|
|
5302
|
+
decimals: scale
|
|
5303
|
+
},
|
|
5304
|
+
counted,
|
|
5305
|
+
unrated
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
//#endregion
|
|
5309
|
+
//#region ../../packages/sdk/src/domain/money/account-positions.ts
|
|
5310
|
+
/**
|
|
5311
|
+
* Lift an Account's raw per-asset positions to the SDK's `AccountPosition`.
|
|
5312
|
+
*
|
|
5313
|
+
* A position whose `assetId` is not a registered asset refuses. An unregistered
|
|
5314
|
+
* token has no identity this SDK can state, so it must never be relabelled as
|
|
5315
|
+
* one — the failure is the point, not a row to skip.
|
|
5316
|
+
*/
|
|
5317
|
+
function accountPositionsFromWire(positions) {
|
|
5318
|
+
return positions.map((position) => {
|
|
5319
|
+
const asset = configuredMoneyAssetById(toAssetId(position.assetId));
|
|
5320
|
+
if (asset === null) throw Errors.invalidInput("assetId", "is not a registered asset");
|
|
5321
|
+
const raw = toWeiAmount(position.rawBalance);
|
|
5322
|
+
const rawAvailable = toWeiAmount(position.rawAvailableBalance ?? position.rawBalance);
|
|
5323
|
+
return {
|
|
5324
|
+
asset: asset.assetId,
|
|
5325
|
+
symbol: asset.symbol,
|
|
5326
|
+
decimals: asset.decimals,
|
|
5327
|
+
balance: assetAmountFromRaw(raw, asset).value,
|
|
5328
|
+
available: assetAmountFromRaw(rawAvailable, asset).value
|
|
5329
|
+
};
|
|
5330
|
+
});
|
|
5331
|
+
}
|
|
5332
|
+
/**
|
|
5333
|
+
* The fiat headline of a set of positions: the sum of the positions the given
|
|
5334
|
+
* oracle can rate into the display currency (ADR-0028 R6, D6). It never picks
|
|
5335
|
+
* a token and never labels one as fiat.
|
|
5336
|
+
*
|
|
5337
|
+
* The oracle is a PARAMETER (D6 obligation 4). It defaults to `PEG_RATES`, which
|
|
5338
|
+
* only rates an asset whose peg equals the display currency, so an unpegged
|
|
5339
|
+
* asset such as WETH stays out of the total and out of `counted`:
|
|
5340
|
+
* a WETH-only Account reads a `$0.00` headline under that default. Hand it a
|
|
5341
|
+
* real oracle and the same Account prices (WETH at `$2000` under
|
|
5342
|
+
* `createMockRates`, a live feed later).
|
|
5343
|
+
*
|
|
5344
|
+
* Either way the unrated position is never zeroed and never dropped: it stays
|
|
5345
|
+
* in `Account.balances`, and `valueIn` reports it in its own `unrated` list so
|
|
5346
|
+
* a caller can say how complete the total is.
|
|
5347
|
+
*/
|
|
5348
|
+
function headlineValuation(balances, displayCurrency, rates = PEG_RATES) {
|
|
5349
|
+
const total = (pick) => valueIn(balances.map((position) => ({
|
|
5350
|
+
asset: position.asset,
|
|
5351
|
+
value: pick(position)
|
|
5352
|
+
})), displayCurrency, rates).total ?? {
|
|
5353
|
+
currency: displayCurrency,
|
|
5354
|
+
value: "0",
|
|
5355
|
+
decimals: 0
|
|
5356
|
+
};
|
|
5357
|
+
return {
|
|
5358
|
+
balance: total((position) => position.balance),
|
|
5359
|
+
available: total((p) => p.available)
|
|
5360
|
+
};
|
|
5361
|
+
}
|
|
5362
|
+
/**
|
|
5363
|
+
* The Account: its per-asset truth plus the fiat headline valuation. The oracle
|
|
5364
|
+
* is threaded from the read that built it, never defaulted past this point
|
|
5365
|
+
* (D6): the account read passes the configured one.
|
|
5366
|
+
*/
|
|
5367
|
+
function accountFromPositions(id, positions, displayCurrency = toCurrencyCode("USD"), rates = PEG_RATES) {
|
|
5368
|
+
const balances = accountPositionsFromWire(positions);
|
|
5369
|
+
return {
|
|
5370
|
+
id,
|
|
5371
|
+
balances,
|
|
5372
|
+
...headlineValuation(balances, displayCurrency, rates)
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
/** The same lift for positions a caller already holds as `AccountPosition`. */
|
|
5376
|
+
function accountFromAccountPositions(id, balances, displayCurrency = toCurrencyCode("USD"), rates = PEG_RATES) {
|
|
5377
|
+
return {
|
|
5378
|
+
id,
|
|
5379
|
+
balances,
|
|
5380
|
+
...headlineValuation(balances, displayCurrency, rates)
|
|
5381
|
+
};
|
|
5382
|
+
}
|
|
5383
|
+
//#endregion
|
|
5211
5384
|
//#region ../../packages/observability/src/operations.ts
|
|
5212
5385
|
const CAPXUL_OPERATIONS = {
|
|
5213
5386
|
account: {
|
|
@@ -5276,6 +5449,26 @@ const CAPXUL_OPERATIONS = {
|
|
|
5276
5449
|
createOrg: "createOrg",
|
|
5277
5450
|
orgs: "orgs"
|
|
5278
5451
|
},
|
|
5452
|
+
/** AUTH A3 (#2073): the SDK facade over the native continuation endpoints. */
|
|
5453
|
+
clientRequests: {
|
|
5454
|
+
decide: "clientRequests.decide",
|
|
5455
|
+
read: "clientRequests.read"
|
|
5456
|
+
},
|
|
5457
|
+
/** AUTH A2 (#2072): the native client authorization issuer and its browser
|
|
5458
|
+
* continuation. One name per durable transition the endpoints can fail on. */
|
|
5459
|
+
clientAuthorization: {
|
|
5460
|
+
acceptRequest: "clientAuthorization.acceptRequest",
|
|
5461
|
+
cancelRequest: "clientAuthorization.cancelRequest",
|
|
5462
|
+
consumeRequest: "clientAuthorization.consumeRequest",
|
|
5463
|
+
denyRequest: "clientAuthorization.denyRequest",
|
|
5464
|
+
invalidateContext: "clientAuthorization.invalidateContext",
|
|
5465
|
+
markConsented: "clientAuthorization.markConsented",
|
|
5466
|
+
readMetadata: "clientAuthorization.readMetadata",
|
|
5467
|
+
revokeGrant: "clientAuthorization.revokeGrant",
|
|
5468
|
+
rotateGrant: "clientAuthorization.rotateGrant",
|
|
5469
|
+
startRequest: "clientAuthorization.startRequest",
|
|
5470
|
+
sweepExpired: "clientAuthorization.sweepExpired"
|
|
5471
|
+
},
|
|
5279
5472
|
credentials: {
|
|
5280
5473
|
bootstrapClient: "credentials.bootstrapClient",
|
|
5281
5474
|
mintQuickstartKey: "credentials.mintQuickstartKey"
|
|
@@ -5774,6 +5967,44 @@ const IdentityRefusedProps = Schema.Struct({
|
|
|
5774
5967
|
state: OptionalString
|
|
5775
5968
|
});
|
|
5776
5969
|
const AuthSignedOutProps = Schema.Struct(TelemetryEnvelopeProps);
|
|
5970
|
+
const NativeAuthorizationMode = Schema.Literals(["login", "signup"]);
|
|
5971
|
+
const NativeAuthorizationStartedProps = Schema.Struct({
|
|
5972
|
+
...TelemetryEnvelopeProps,
|
|
5973
|
+
mode: NativeAuthorizationMode
|
|
5974
|
+
});
|
|
5975
|
+
const NativeAuthorizationReviewedProps = Schema.Struct({
|
|
5976
|
+
...TelemetryEnvelopeProps,
|
|
5977
|
+
mode: NativeAuthorizationMode
|
|
5978
|
+
});
|
|
5979
|
+
const NativeAuthorizationAcceptedProps = Schema.Struct({
|
|
5980
|
+
...TelemetryEnvelopeProps,
|
|
5981
|
+
mode: NativeAuthorizationMode,
|
|
5982
|
+
/** Whether consent also had to finish account setup first. */
|
|
5983
|
+
setup_state: Schema.optional(Schema.Literals([
|
|
5984
|
+
"not_required",
|
|
5985
|
+
"new",
|
|
5986
|
+
"resumable"
|
|
5987
|
+
]))
|
|
5988
|
+
});
|
|
5989
|
+
const NativeAuthorizationDeniedProps = Schema.Struct({
|
|
5990
|
+
...TelemetryEnvelopeProps,
|
|
5991
|
+
mode: NativeAuthorizationMode
|
|
5992
|
+
});
|
|
5993
|
+
const NativeAuthorizationConnectedProps = Schema.Struct({
|
|
5994
|
+
...TelemetryEnvelopeProps,
|
|
5995
|
+
mode: NativeAuthorizationMode
|
|
5996
|
+
});
|
|
5997
|
+
const NativeAuthorizationFailedProps = Schema.Struct({
|
|
5998
|
+
...TelemetryEnvelopeProps,
|
|
5999
|
+
mode: Schema.optional(NativeAuthorizationMode),
|
|
6000
|
+
/** The refusal word the store returned, for example `identity_mismatch` or
|
|
6001
|
+
* `request_expired`. These are the closed `CLIENT_AUTHORIZATION_REFUSALS` from
|
|
6002
|
+
* `@capxul/wire` plus the store-internal `fresh_session_required` (ruling R3),
|
|
6003
|
+
* which the continuation maps to `identity_mismatch` on the wire but reports
|
|
6004
|
+
* truthfully here. Kept as `OptionalString` rather than an imported literal
|
|
6005
|
+
* set: this package must not depend on the transport contract. */
|
|
6006
|
+
reason: OptionalString
|
|
6007
|
+
});
|
|
5777
6008
|
const ProvisioningSafeCreatedProps = Schema.Struct({
|
|
5778
6009
|
...TelemetryEnvelopeProps,
|
|
5779
6010
|
safe_address: OptionalAddress
|
|
@@ -5816,6 +6047,12 @@ const BootstrapFailedProps = Schema.Struct({
|
|
|
5816
6047
|
* Organization row's chain, the configured token address — never from a label.
|
|
5817
6048
|
* The verified `organization_id` and its group arrive from the backend
|
|
5818
6049
|
* attribution, so they are not repeated here.
|
|
6050
|
+
*
|
|
6051
|
+
* D1 (2026-09-10): `asset_id` carries the canonical branded identity the read
|
|
6052
|
+
* resolved, `eip155:<chainId>/erc20:<lowercase-address>`, from the registry
|
|
6053
|
+
* row. `token_address`/`chain_id` are its parts; `currency` is the peg and a
|
|
6054
|
+
* display code. The producer emits the branded id so the live read can be
|
|
6055
|
+
* grouped by ASSET, never by symbol or peg.
|
|
5819
6056
|
*/
|
|
5820
6057
|
const AccountBalanceReadProps = Schema.Struct({
|
|
5821
6058
|
...TelemetryEnvelopeProps,
|
|
@@ -5823,6 +6060,7 @@ const AccountBalanceReadProps = Schema.Struct({
|
|
|
5823
6060
|
account_kind: OptionalAccountKind,
|
|
5824
6061
|
chain_id: OptionalChainId,
|
|
5825
6062
|
token_address: OptionalAddress,
|
|
6063
|
+
asset_id: OptionalString,
|
|
5826
6064
|
currency: OptionalCurrencyCode,
|
|
5827
6065
|
balance_bucket: OptionalBalanceBucket,
|
|
5828
6066
|
durationMs: OptionalDurationMs
|
|
@@ -6291,6 +6529,30 @@ const AuthSignedOutTelemetryEventSchema = Schema.Struct({
|
|
|
6291
6529
|
name: Schema.Literal("auth_signed_out"),
|
|
6292
6530
|
props: AuthSignedOutProps
|
|
6293
6531
|
});
|
|
6532
|
+
const NativeAuthorizationStartedTelemetryEventSchema = Schema.Struct({
|
|
6533
|
+
name: Schema.Literal("native_authorization_started"),
|
|
6534
|
+
props: NativeAuthorizationStartedProps
|
|
6535
|
+
});
|
|
6536
|
+
const NativeAuthorizationReviewedTelemetryEventSchema = Schema.Struct({
|
|
6537
|
+
name: Schema.Literal("native_authorization_reviewed"),
|
|
6538
|
+
props: NativeAuthorizationReviewedProps
|
|
6539
|
+
});
|
|
6540
|
+
const NativeAuthorizationAcceptedTelemetryEventSchema = Schema.Struct({
|
|
6541
|
+
name: Schema.Literal("native_authorization_accepted"),
|
|
6542
|
+
props: NativeAuthorizationAcceptedProps
|
|
6543
|
+
});
|
|
6544
|
+
const NativeAuthorizationDeniedTelemetryEventSchema = Schema.Struct({
|
|
6545
|
+
name: Schema.Literal("native_authorization_denied"),
|
|
6546
|
+
props: NativeAuthorizationDeniedProps
|
|
6547
|
+
});
|
|
6548
|
+
const NativeAuthorizationConnectedTelemetryEventSchema = Schema.Struct({
|
|
6549
|
+
name: Schema.Literal("native_authorization_connected"),
|
|
6550
|
+
props: NativeAuthorizationConnectedProps
|
|
6551
|
+
});
|
|
6552
|
+
const NativeAuthorizationFailedTelemetryEventSchema = Schema.Struct({
|
|
6553
|
+
name: Schema.Literal("native_authorization_failed"),
|
|
6554
|
+
props: NativeAuthorizationFailedProps
|
|
6555
|
+
});
|
|
6294
6556
|
const OnboardingIntentSelectedTelemetryEventSchema = Schema.Struct({
|
|
6295
6557
|
name: Schema.Literal("onboarding_intent_selected"),
|
|
6296
6558
|
props: OnboardingIntentSelectedProps
|
|
@@ -6545,12 +6807,18 @@ const CLI_COMMAND_NAMES = [
|
|
|
6545
6807
|
"telemetry.status",
|
|
6546
6808
|
"telemetry.enable",
|
|
6547
6809
|
"telemetry.disable",
|
|
6810
|
+
"auth",
|
|
6811
|
+
"auth.login",
|
|
6812
|
+
"auth.signup",
|
|
6813
|
+
"auth.status",
|
|
6814
|
+
"auth.logout",
|
|
6548
6815
|
"unknown"
|
|
6549
6816
|
];
|
|
6550
6817
|
const CLI_CAPABILITIES = [
|
|
6551
6818
|
"cli",
|
|
6552
6819
|
"diagnostics",
|
|
6553
|
-
"collection"
|
|
6820
|
+
"collection",
|
|
6821
|
+
"authentication"
|
|
6554
6822
|
];
|
|
6555
6823
|
const CLI_INVOCATION_KINDS = [
|
|
6556
6824
|
"command",
|
|
@@ -6568,8 +6836,9 @@ const CliInvocationProps = {
|
|
|
6568
6836
|
capability: Schema.Literals(CLI_CAPABILITIES),
|
|
6569
6837
|
invocation_kind: Schema.Literals(CLI_INVOCATION_KINDS),
|
|
6570
6838
|
invocation_id: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u)),
|
|
6571
|
-
cli_identity_kind: Schema.optional(Schema.
|
|
6839
|
+
cli_identity_kind: Schema.optional(Schema.Literals(["anonymous", "identified"])),
|
|
6572
6840
|
anonymous_id: Schema.optional(Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u))),
|
|
6841
|
+
auth_user_id: Schema.optional(Schema.String.check(Schema.isPattern(/^[^\s]{1,256}$/u))),
|
|
6573
6842
|
occurred_at: Schema.Number,
|
|
6574
6843
|
cli_version: CliRelease,
|
|
6575
6844
|
sdk_version: CliRelease,
|
|
@@ -6936,6 +7205,12 @@ const TELEMETRY_EVENT_SCHEMAS = {
|
|
|
6936
7205
|
auth_failed: AuthFailedTelemetryEventSchema,
|
|
6937
7206
|
identity_refused: IdentityRefusedTelemetryEventSchema,
|
|
6938
7207
|
auth_signed_out: AuthSignedOutTelemetryEventSchema,
|
|
7208
|
+
native_authorization_started: NativeAuthorizationStartedTelemetryEventSchema,
|
|
7209
|
+
native_authorization_reviewed: NativeAuthorizationReviewedTelemetryEventSchema,
|
|
7210
|
+
native_authorization_accepted: NativeAuthorizationAcceptedTelemetryEventSchema,
|
|
7211
|
+
native_authorization_denied: NativeAuthorizationDeniedTelemetryEventSchema,
|
|
7212
|
+
native_authorization_connected: NativeAuthorizationConnectedTelemetryEventSchema,
|
|
7213
|
+
native_authorization_failed: NativeAuthorizationFailedTelemetryEventSchema,
|
|
6939
7214
|
onboarding_intent_selected: OnboardingIntentSelectedTelemetryEventSchema,
|
|
6940
7215
|
onboarding_profile_submitted: OnboardingProfileSubmittedTelemetryEventSchema,
|
|
6941
7216
|
onboarding_organization_submitted: OnboardingOrganizationSubmittedTelemetryEventSchema,
|
|
@@ -9361,7 +9636,7 @@ function isSafeField(field, value) {
|
|
|
9361
9636
|
}
|
|
9362
9637
|
//#endregion
|
|
9363
9638
|
//#region ../../packages/wire/src/client-authorization.ts
|
|
9364
|
-
const NATIVE_CLIENT_ID = "capxul-cli";
|
|
9639
|
+
const NATIVE_CLIENT_ID$1 = "capxul-cli";
|
|
9365
9640
|
const NATIVE_CLIENT_SCOPE = "account:read";
|
|
9366
9641
|
const NATIVE_CLIENT_LIMITS = {
|
|
9367
9642
|
accessMs: 36e5,
|
|
@@ -9388,8 +9663,7 @@ const country = normalize((s) => s.toUpperCase()).pipe(Schema.refine((s) => COUN
|
|
|
9388
9663
|
const credential = Schema.RedactedFromValue(Schema.String.check(Schema.isMinLength(32), Schema.isMaxLength(8192), Schema.isPattern(/^[A-Za-z0-9._~-]+$/u)), { label: "native authorization credential" });
|
|
9389
9664
|
const otp = Schema.RedactedFromValue(Schema.String.check(Schema.isPattern(/^[0-9]{6}$/u)), { label: "OTP" });
|
|
9390
9665
|
const scopes = Schema.Tuple([Schema.Literal(NATIVE_CLIENT_SCOPE)]);
|
|
9391
|
-
|
|
9392
|
-
const NativeHttpsOriginSchema = Schema.String.check(Schema.makeFilter((value) => {
|
|
9666
|
+
const isNativeHttpsOriginValue = (value) => {
|
|
9393
9667
|
try {
|
|
9394
9668
|
const url = new URL(value);
|
|
9395
9669
|
const host = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
@@ -9397,20 +9671,43 @@ const NativeHttpsOriginSchema = Schema.String.check(Schema.makeFilter((value) =>
|
|
|
9397
9671
|
} catch {
|
|
9398
9672
|
return false;
|
|
9399
9673
|
}
|
|
9400
|
-
}
|
|
9674
|
+
};
|
|
9675
|
+
Schema.String.check(Schema.makeFilter(isNativeHttpsOriginValue, { message: "must be a canonical credential-free HTTPS origin outside retired Convex cloud hosts" }));
|
|
9676
|
+
const isLoopbackOriginValue = (value) => {
|
|
9677
|
+
try {
|
|
9678
|
+
const url = new URL(value);
|
|
9679
|
+
const host = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
9680
|
+
return url.protocol === "http:" && value === url.origin && !url.username && !url.password && !url.search && !url.hash && (host === "127.0.0.1" || host === "[::1]" || host === "localhost");
|
|
9681
|
+
} catch {
|
|
9682
|
+
return false;
|
|
9683
|
+
}
|
|
9684
|
+
};
|
|
9685
|
+
Schema.String.check(Schema.makeFilter(isLoopbackOriginValue, { message: "must be a canonical credential-free loopback HTTP origin" }));
|
|
9686
|
+
/**
|
|
9687
|
+
* Either a standing-tier HTTPS origin or a devnet loopback origin. A `Union` of
|
|
9688
|
+
* the two refined string schemas does not type-check in this Effect version —
|
|
9689
|
+
* it widens both fields to `unknown` — so the disjunction lives in one filter
|
|
9690
|
+
* and the struct check below is what binds a loopback origin to
|
|
9691
|
+
* `environment: "local"`. Loopback is never admissible on a standing tier.
|
|
9692
|
+
*/
|
|
9693
|
+
const NativeOriginSchema = Schema.String.check(Schema.makeFilter((value) => isNativeHttpsOriginValue(value) || isLoopbackOriginValue(value), { message: "must be a canonical credential-free HTTPS origin outside retired Convex cloud hosts, or a loopback HTTP origin for the local devnet" }));
|
|
9401
9694
|
const NativeClientConfigurationSchema = Schema.Struct({
|
|
9402
|
-
clientId: Schema.Literal(NATIVE_CLIENT_ID),
|
|
9695
|
+
clientId: Schema.Literal(NATIVE_CLIENT_ID$1),
|
|
9403
9696
|
applicationId: AppIdSchema,
|
|
9404
9697
|
keyId,
|
|
9405
9698
|
environment: Schema.Literals([
|
|
9699
|
+
"local",
|
|
9406
9700
|
"development",
|
|
9407
9701
|
"staging",
|
|
9408
9702
|
"production"
|
|
9409
9703
|
]),
|
|
9410
9704
|
keyEnvironment: Schema.Literals(["test", "live"]),
|
|
9411
|
-
issuer:
|
|
9412
|
-
verificationOrigin:
|
|
9413
|
-
}).annotate(strict).check(Schema.makeFilter((c) => c.keyEnvironment === (c.environment === "production" ? "live" : "test"), { message: "key environment must match the configured deployment" })).
|
|
9705
|
+
issuer: NativeOriginSchema,
|
|
9706
|
+
verificationOrigin: NativeOriginSchema
|
|
9707
|
+
}).annotate(strict).check(Schema.makeFilter((c) => c.keyEnvironment === (c.environment === "production" ? "live" : "test"), { message: "key environment must match the configured deployment" })).check(Schema.makeFilter((c) => {
|
|
9708
|
+
const loopback = isLoopbackOriginValue(c.issuer) || isLoopbackOriginValue(c.verificationOrigin);
|
|
9709
|
+
return c.environment === "local" ? isLoopbackOriginValue(c.issuer) && isLoopbackOriginValue(c.verificationOrigin) : !loopback;
|
|
9710
|
+
}, { message: "loopback origins belong to the local devnet environment alone" })).annotate(strict);
|
|
9414
9711
|
const NativeSignupProfileSchema = Schema.Struct({
|
|
9415
9712
|
displayName: normalize((s) => s.trim()).check(Schema.isMinLength(1), Schema.isMaxLength(200)),
|
|
9416
9713
|
country,
|
|
@@ -9441,7 +9738,7 @@ const ClientAuthorizationIntentSchema = Schema.Union([Schema.Struct({ mode: Sche
|
|
|
9441
9738
|
profile: NativeSignupProfileSchema
|
|
9442
9739
|
}).annotate(strict)]).annotate(strict);
|
|
9443
9740
|
Schema.Struct({
|
|
9444
|
-
clientId: Schema.Literal(NATIVE_CLIENT_ID),
|
|
9741
|
+
clientId: Schema.Literal(NATIVE_CLIENT_ID$1),
|
|
9445
9742
|
email,
|
|
9446
9743
|
intent: ClientAuthorizationIntentSchema,
|
|
9447
9744
|
waitMs: Schema.Number.check(Schema.isInt(), Schema.isBetween({
|
|
@@ -9464,7 +9761,7 @@ const profileSummary = {
|
|
|
9464
9761
|
handle,
|
|
9465
9762
|
country
|
|
9466
9763
|
};
|
|
9467
|
-
Schema.Union([
|
|
9764
|
+
const ClientRequestViewSchema = Schema.Union([
|
|
9468
9765
|
Schema.Struct({
|
|
9469
9766
|
...request,
|
|
9470
9767
|
phase: Schema.Literal(CLIENT_AUTHORIZATION_PHASE.OTP_REQUIRED),
|
|
@@ -9529,13 +9826,12 @@ Schema.Struct({
|
|
|
9529
9826
|
code: otp
|
|
9530
9827
|
}).annotate(strict);
|
|
9531
9828
|
Schema.Struct({
|
|
9532
|
-
client_id: Schema.Literal(NATIVE_CLIENT_ID),
|
|
9829
|
+
client_id: Schema.Literal(NATIVE_CLIENT_ID$1),
|
|
9533
9830
|
grant_type: Schema.Literal("refresh_token"),
|
|
9534
|
-
refresh_token: credential
|
|
9535
|
-
refreshAttemptId: Schema.RedactedFromValue(Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{32,128}$/u)), { label: "refresh attempt" })
|
|
9831
|
+
refresh_token: credential
|
|
9536
9832
|
}).annotate(strict);
|
|
9537
9833
|
Schema.Struct({
|
|
9538
|
-
client_id: Schema.Literal(NATIVE_CLIENT_ID),
|
|
9834
|
+
client_id: Schema.Literal(NATIVE_CLIENT_ID$1),
|
|
9539
9835
|
applicationId: AppIdSchema,
|
|
9540
9836
|
resource: text$2(2048),
|
|
9541
9837
|
token_type: Schema.Literal("Bearer"),
|
|
@@ -9585,7 +9881,7 @@ Schema.Struct({
|
|
|
9585
9881
|
return m.resource === expected.resource && m.device_authorization_endpoint === expected.device_authorization_endpoint && m.token_endpoint === expected.token_endpoint && m.revocation_endpoint === expected.revocation_endpoint && m.session_endpoint === expected.session_endpoint && m.verification_uri === expected.verification_uri;
|
|
9586
9882
|
}, { message: "native endpoints must match their configured issuer and verification origin" })).annotate(strict);
|
|
9587
9883
|
Schema.Struct({
|
|
9588
|
-
client_id: Schema.Literal(NATIVE_CLIENT_ID),
|
|
9884
|
+
client_id: Schema.Literal(NATIVE_CLIENT_ID$1),
|
|
9589
9885
|
grant_type: Schema.Literal("urn:ietf:params:oauth:grant-type:device_code"),
|
|
9590
9886
|
device_code: credential
|
|
9591
9887
|
}).annotate(strict);
|
|
@@ -9604,6 +9900,33 @@ Schema.Struct({
|
|
|
9604
9900
|
maximum: NATIVE_CLIENT_LIMITS.requestMs / 1e3
|
|
9605
9901
|
}))
|
|
9606
9902
|
}).annotate(strict);
|
|
9903
|
+
Schema.Struct({
|
|
9904
|
+
client_id: Schema.Literal(NATIVE_CLIENT_ID$1),
|
|
9905
|
+
token: credential,
|
|
9906
|
+
token_type_hint: Schema.optional(Schema.Literals([
|
|
9907
|
+
"access_token",
|
|
9908
|
+
"refresh_token",
|
|
9909
|
+
"device_code"
|
|
9910
|
+
]))
|
|
9911
|
+
}).annotate(strict);
|
|
9912
|
+
Schema.Struct({
|
|
9913
|
+
client_id: Schema.Literal(NATIVE_CLIENT_ID$1),
|
|
9914
|
+
token: credential
|
|
9915
|
+
}).annotate(strict);
|
|
9916
|
+
Schema.Struct({
|
|
9917
|
+
error: Schema.Literals([
|
|
9918
|
+
"authorization_pending",
|
|
9919
|
+
"slow_down",
|
|
9920
|
+
"access_denied",
|
|
9921
|
+
"invalid_grant",
|
|
9922
|
+
"invalid_request",
|
|
9923
|
+
"expired_token",
|
|
9924
|
+
"server_error"
|
|
9925
|
+
]),
|
|
9926
|
+
refusal: Schema.optional(Schema.Literals(CLIENT_AUTHORIZATION_REFUSALS)),
|
|
9927
|
+
retryable: Schema.Boolean,
|
|
9928
|
+
retry_after: Schema.optional(nonNegativeInt)
|
|
9929
|
+
}).annotate(strict);
|
|
9607
9930
|
//#endregion
|
|
9608
9931
|
//#region ../../packages/sdk/src/internal/invocation-observation.ts
|
|
9609
9932
|
const INVOCATION_OBSERVATION = Symbol("capxul.invocation-observation");
|
|
@@ -12057,7 +12380,7 @@ function subscribeActivity(deps, actor, controls, onChange) {
|
|
|
12057
12380
|
}
|
|
12058
12381
|
//#endregion
|
|
12059
12382
|
//#region ../../packages/sdk/package.json
|
|
12060
|
-
var version$1 = "4.20.0-beta.
|
|
12383
|
+
var version$1 = "4.20.0-beta.5";
|
|
12061
12384
|
//#endregion
|
|
12062
12385
|
//#region ../../packages/sdk/src/telemetry/exception-projection.ts
|
|
12063
12386
|
/** Fixed fallback for failures that have no safe message. */
|
|
@@ -14696,7 +15019,7 @@ const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
|
|
|
14696
15019
|
["command", CLI_COMMAND_NAMES],
|
|
14697
15020
|
["capability", CLI_CAPABILITIES],
|
|
14698
15021
|
["invocation_kind", CLI_INVOCATION_KINDS],
|
|
14699
|
-
["cli_identity_kind", ["anonymous"]],
|
|
15022
|
+
["cli_identity_kind", ["anonymous", "identified"]],
|
|
14700
15023
|
["output_mode", ["human", "json"]],
|
|
14701
15024
|
["unit", [
|
|
14702
15025
|
"1",
|
|
@@ -19613,13 +19936,15 @@ var ConvexAccountAdapter = class {
|
|
|
19613
19936
|
#convex;
|
|
19614
19937
|
#fns;
|
|
19615
19938
|
#telemetry;
|
|
19939
|
+
#rates;
|
|
19616
19940
|
constructor(deps) {
|
|
19617
19941
|
this.#convex = deps.convex;
|
|
19618
19942
|
this.#fns = deps.functions ?? DEFAULT_FUNCTIONS$3;
|
|
19619
19943
|
this.#telemetry = deps.telemetry;
|
|
19944
|
+
this.#rates = deps.rates ?? MONEY_READ_RATES;
|
|
19620
19945
|
}
|
|
19621
19946
|
readBalance(input) {
|
|
19622
|
-
return retryIdempotentRead(this.#convex.action(this.#fns.readBalance, copyInvocationObservation(input, { chainId: wireChainId(input.chainId) })), CAPXUL_OPERATIONS.accounts.read, this.#telemetry, input).pipe(Effect.mapError((error) => accountReadErrorFromCapxul("readBalance", error.publicError, error)), Effect.flatMap((wire) => brandAccountEffect("readBalance", wire)), Effect.catchDefect((cause) => Effect.fail(accountReadErrorFromUnknown("readBalance", cause))));
|
|
19947
|
+
return retryIdempotentRead(this.#convex.action(this.#fns.readBalance, copyInvocationObservation(input, { chainId: wireChainId(input.chainId) })), CAPXUL_OPERATIONS.accounts.read, this.#telemetry, input).pipe(Effect.mapError((error) => accountReadErrorFromCapxul("readBalance", error.publicError, error)), Effect.flatMap((wire) => brandAccountEffect("readBalance", wire, this.#rates)), Effect.catchDefect((cause) => Effect.fail(accountReadErrorFromUnknown("readBalance", cause))));
|
|
19623
19948
|
}
|
|
19624
19949
|
fundFromFaucet(input) {
|
|
19625
19950
|
const operation = "fundFromFaucet";
|
|
@@ -19632,26 +19957,21 @@ var ConvexAccountAdapter = class {
|
|
|
19632
19957
|
}).pipe(Effect.mapError((error) => accountReadErrorFromCapxul(operation, error.publicError, error)), Effect.map((wire) => ({ txHash: wire.txHash })), Effect.catchDefect((cause) => Effect.fail(accountReadErrorFromUnknown(operation, cause))));
|
|
19633
19958
|
}
|
|
19634
19959
|
};
|
|
19635
|
-
function ConvexAccountLayer() {
|
|
19960
|
+
function ConvexAccountLayer(input = {}) {
|
|
19636
19961
|
return Layer.effect(AccountReadPortTag, Effect.all([ConvexCallPortTag, TelemetryPortTag]).pipe(Effect.map(([convex, telemetry]) => new ConvexAccountAdapter({
|
|
19637
19962
|
convex,
|
|
19638
|
-
telemetry
|
|
19963
|
+
telemetry,
|
|
19964
|
+
...input
|
|
19639
19965
|
}))));
|
|
19640
19966
|
}
|
|
19641
|
-
function brandAccountEffect(operation, wire) {
|
|
19967
|
+
function brandAccountEffect(operation, wire, rates) {
|
|
19642
19968
|
return Effect.try({
|
|
19643
|
-
try: () => brandAccount(wire),
|
|
19969
|
+
try: () => brandAccount(wire, rates),
|
|
19644
19970
|
catch: (cause) => accountReadErrorFromUnknown(operation, cause)
|
|
19645
19971
|
});
|
|
19646
19972
|
}
|
|
19647
|
-
function brandAccount(wire) {
|
|
19648
|
-
|
|
19649
|
-
const available = fromWei(wire.rawAvailableBalance ?? wire.rawBalance, wire.decimals, wire.currency);
|
|
19650
|
-
return {
|
|
19651
|
-
id: toAccountId(wire.accountId),
|
|
19652
|
-
balance,
|
|
19653
|
-
available
|
|
19654
|
-
};
|
|
19973
|
+
function brandAccount(wire, rates) {
|
|
19974
|
+
return accountFromPositions(toAccountId(wire.accountId), wire.positions, void 0, rates);
|
|
19655
19975
|
}
|
|
19656
19976
|
function accountReadErrorFromUnknown(operation, cause) {
|
|
19657
19977
|
if (cause instanceof CapxulError) return accountReadErrorFromCapxul(operation, cause);
|
|
@@ -19790,16 +20110,16 @@ function brandOrgView(wire, treasury, viewerRole) {
|
|
|
19790
20110
|
};
|
|
19791
20111
|
}
|
|
19792
20112
|
/**
|
|
19793
|
-
* Build the Org treasury `Account` from the raw
|
|
19794
|
-
* (the D3 RPC
|
|
19795
|
-
* partition yet (a fresh Org reads back
|
|
20113
|
+
* Build the Org treasury `Account` from the raw per-asset `balanceOf` reads
|
|
20114
|
+
* (the D3 RPC pull). `available === balance` for a treasury with no envelope
|
|
20115
|
+
* partition yet (a fresh Org reads back a zero position per asset).
|
|
20116
|
+
*
|
|
20117
|
+
* `rates` is the oracle the headline prices with (D6). The live read passes the
|
|
20118
|
+
* configured one; it defaults to `PEG_RATES` so an unpegged holding reads as
|
|
20119
|
+
* unrated rather than as a number nobody priced.
|
|
19796
20120
|
*/
|
|
19797
20121
|
function brandOrgTreasury(input) {
|
|
19798
|
-
return {
|
|
19799
|
-
id: toAccountId(`account_${orgIdBody(input.orgId)}`),
|
|
19800
|
-
balance: input.money,
|
|
19801
|
-
available: input.money
|
|
19802
|
-
};
|
|
20122
|
+
return accountFromAccountPositions(toAccountId(`account_${orgIdBody(input.orgId)}`), input.balances, void 0, input.rates);
|
|
19803
20123
|
}
|
|
19804
20124
|
function brandOrgRole(wire) {
|
|
19805
20125
|
return {
|
|
@@ -19886,10 +20206,12 @@ var ConvexOrganizationAdapter = class {
|
|
|
19886
20206
|
#convex;
|
|
19887
20207
|
#fns;
|
|
19888
20208
|
#telemetry;
|
|
20209
|
+
#rates;
|
|
19889
20210
|
constructor(input) {
|
|
19890
20211
|
this.#convex = input.convex;
|
|
19891
20212
|
this.#fns = input.functions ?? DEFAULT_FUNCTIONS$1;
|
|
19892
20213
|
this.#telemetry = input.telemetry;
|
|
20214
|
+
this.#rates = input.rates ?? MONEY_READ_RATES;
|
|
19893
20215
|
}
|
|
19894
20216
|
createOrg(_input) {
|
|
19895
20217
|
return Effect.fail(orgErrorFromCapxul("createOrg", Errors.notImplemented("organizationSetup", "use onboarding.completeOrganization")));
|
|
@@ -19937,15 +20259,11 @@ var ConvexOrganizationAdapter = class {
|
|
|
19937
20259
|
const orgId = String(input.orgId);
|
|
19938
20260
|
return retryIdempotentRead(this.#convex.action(this.#fns.readTreasury, copyInvocationObservation(input, { orgId })), CAPXUL_OPERATIONS.org.treasury, this.#telemetry, input).pipe(Effect.mapError((error) => orgErrorFromCapxul("readTreasury", error.publicError, error)), Effect.map((wire) => {
|
|
19939
20261
|
if (wire.orgId !== orgId) throw Errors.invalidInput("orgId", "Organization treasury scope does not match");
|
|
19940
|
-
|
|
19941
|
-
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
money: balance
|
|
19946
|
-
}),
|
|
19947
|
-
available
|
|
19948
|
-
};
|
|
20262
|
+
return brandOrgTreasury({
|
|
20263
|
+
orgId: wire.orgId,
|
|
20264
|
+
balances: accountPositionsFromWire(wire.positions),
|
|
20265
|
+
rates: this.#rates
|
|
20266
|
+
});
|
|
19949
20267
|
}));
|
|
19950
20268
|
}
|
|
19951
20269
|
};
|
|
@@ -45724,6 +46042,157 @@ function makeAuthMethods(deps) {
|
|
|
45724
46042
|
};
|
|
45725
46043
|
}
|
|
45726
46044
|
//#endregion
|
|
46045
|
+
//#region ../../packages/sdk/src/surface/client-requests.ts
|
|
46046
|
+
function decodeView(body) {
|
|
46047
|
+
try {
|
|
46048
|
+
return {
|
|
46049
|
+
ok: true,
|
|
46050
|
+
value: Schema.decodeUnknownSync(ClientRequestViewSchema)(body)
|
|
46051
|
+
};
|
|
46052
|
+
} catch {
|
|
46053
|
+
return {
|
|
46054
|
+
ok: false,
|
|
46055
|
+
error: Errors.invalidInput("clientRequest", "view is not a closed phase")
|
|
46056
|
+
};
|
|
46057
|
+
}
|
|
46058
|
+
}
|
|
46059
|
+
function httpError$1(status) {
|
|
46060
|
+
if (status === 401) return {
|
|
46061
|
+
ok: false,
|
|
46062
|
+
error: Errors.notAuthenticated()
|
|
46063
|
+
};
|
|
46064
|
+
if (status === 404) return {
|
|
46065
|
+
ok: false,
|
|
46066
|
+
error: Errors.invalidInput("requestId", "is not a live request")
|
|
46067
|
+
};
|
|
46068
|
+
if (status === 429) return {
|
|
46069
|
+
ok: false,
|
|
46070
|
+
error: Errors.rateLimited({ resource: "clientRequest" })
|
|
46071
|
+
};
|
|
46072
|
+
return {
|
|
46073
|
+
ok: false,
|
|
46074
|
+
error: Errors.networkError("clientRequests")
|
|
46075
|
+
};
|
|
46076
|
+
}
|
|
46077
|
+
function makeClientRequestMethods(deps) {
|
|
46078
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
46079
|
+
const urlFor = (path, query) => {
|
|
46080
|
+
const resolved = resolveAuthClientUrl(deps.authBaseUrl, path);
|
|
46081
|
+
return query === void 0 ? resolved : `${resolved}?${query}`;
|
|
46082
|
+
};
|
|
46083
|
+
async function readView(requestId, options) {
|
|
46084
|
+
if (options?.signal?.aborted) return {
|
|
46085
|
+
ok: false,
|
|
46086
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.clientRequests.read })
|
|
46087
|
+
};
|
|
46088
|
+
try {
|
|
46089
|
+
const response = await fetchImpl(urlFor("/api/auth/native/request", `requestId=${encodeURIComponent(requestId)}`), {
|
|
46090
|
+
method: "GET",
|
|
46091
|
+
credentials: "include",
|
|
46092
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
46093
|
+
});
|
|
46094
|
+
const body = await response.json().catch(() => null);
|
|
46095
|
+
if (!response.ok) {
|
|
46096
|
+
const viewed = decodeView(body);
|
|
46097
|
+
return viewed.ok ? viewed : httpError$1(response.status);
|
|
46098
|
+
}
|
|
46099
|
+
return decodeView(body);
|
|
46100
|
+
} catch (cause) {
|
|
46101
|
+
if (options?.signal?.aborted) return {
|
|
46102
|
+
ok: false,
|
|
46103
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.clientRequests.read })
|
|
46104
|
+
};
|
|
46105
|
+
if (isCapxulError(cause)) return {
|
|
46106
|
+
ok: false,
|
|
46107
|
+
error: cause
|
|
46108
|
+
};
|
|
46109
|
+
return {
|
|
46110
|
+
ok: false,
|
|
46111
|
+
error: Errors.networkError("native request read failed")
|
|
46112
|
+
};
|
|
46113
|
+
}
|
|
46114
|
+
}
|
|
46115
|
+
async function postDecision(path, input, options) {
|
|
46116
|
+
if (options?.signal?.aborted) return {
|
|
46117
|
+
ok: false,
|
|
46118
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.clientRequests.decide })
|
|
46119
|
+
};
|
|
46120
|
+
try {
|
|
46121
|
+
const response = await fetchImpl(urlFor(path), {
|
|
46122
|
+
method: "POST",
|
|
46123
|
+
credentials: "include",
|
|
46124
|
+
headers: { "content-type": "application/json" },
|
|
46125
|
+
body: JSON.stringify({
|
|
46126
|
+
requestId: input.requestId,
|
|
46127
|
+
revision: input.revision
|
|
46128
|
+
}),
|
|
46129
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
46130
|
+
});
|
|
46131
|
+
const viewed = decodeView(await response.json().catch(() => null));
|
|
46132
|
+
if (viewed.ok) return viewed;
|
|
46133
|
+
if (!response.ok) return httpError$1(response.status);
|
|
46134
|
+
return viewed;
|
|
46135
|
+
} catch (cause) {
|
|
46136
|
+
if (options?.signal?.aborted) return {
|
|
46137
|
+
ok: false,
|
|
46138
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.clientRequests.decide })
|
|
46139
|
+
};
|
|
46140
|
+
if (isCapxulError(cause)) return {
|
|
46141
|
+
ok: false,
|
|
46142
|
+
error: cause
|
|
46143
|
+
};
|
|
46144
|
+
return {
|
|
46145
|
+
ok: false,
|
|
46146
|
+
error: Errors.networkError("native request decision failed")
|
|
46147
|
+
};
|
|
46148
|
+
}
|
|
46149
|
+
}
|
|
46150
|
+
return {
|
|
46151
|
+
read: (input, options) => readView(input.requestId, options),
|
|
46152
|
+
async sendOtp(input, options) {
|
|
46153
|
+
const view = await readView(input.requestId, options);
|
|
46154
|
+
if (!view.ok) return view;
|
|
46155
|
+
if (view.value.phase !== "otp_required" && view.value.phase !== "review") return {
|
|
46156
|
+
ok: false,
|
|
46157
|
+
error: Errors.wrongState({
|
|
46158
|
+
method: "clientRequests.sendOtp",
|
|
46159
|
+
currentState: view.value.phase,
|
|
46160
|
+
validStates: ["otp_required", "review"]
|
|
46161
|
+
})
|
|
46162
|
+
};
|
|
46163
|
+
const sent = await deps.signIn({ email: view.value.email }, options);
|
|
46164
|
+
if (!sent.ok) return sent;
|
|
46165
|
+
return {
|
|
46166
|
+
ok: true,
|
|
46167
|
+
value: void 0
|
|
46168
|
+
};
|
|
46169
|
+
},
|
|
46170
|
+
async verifyOtp(input, options) {
|
|
46171
|
+
const view = await readView(input.requestId, options);
|
|
46172
|
+
if (!view.ok) return view;
|
|
46173
|
+
if (view.value.phase !== "otp_required" && view.value.phase !== "review") return {
|
|
46174
|
+
ok: false,
|
|
46175
|
+
error: Errors.wrongState({
|
|
46176
|
+
method: "clientRequests.verifyOtp",
|
|
46177
|
+
currentState: view.value.phase,
|
|
46178
|
+
validStates: ["otp_required", "review"]
|
|
46179
|
+
})
|
|
46180
|
+
};
|
|
46181
|
+
const verified = await deps.verifyOtp({
|
|
46182
|
+
email: view.value.email,
|
|
46183
|
+
code: input.code
|
|
46184
|
+
}, options);
|
|
46185
|
+
if (!verified.ok) return verified;
|
|
46186
|
+
return {
|
|
46187
|
+
ok: true,
|
|
46188
|
+
value: void 0
|
|
46189
|
+
};
|
|
46190
|
+
},
|
|
46191
|
+
accept: (input, options) => postDecision("/api/auth/native/request/accept", input, options),
|
|
46192
|
+
cancel: (input, options) => postDecision("/api/auth/native/request/deny", input, options)
|
|
46193
|
+
};
|
|
46194
|
+
}
|
|
46195
|
+
//#endregion
|
|
45727
46196
|
//#region ../../packages/sdk/src/surface/smart-account-deps.ts
|
|
45728
46197
|
/** The single Context tag the `smartAccount.*` methods resolve. */
|
|
45729
46198
|
var SmartAccountDepsTag = class extends Context.Service()("@capxul/sdk/SmartAccountDeps") {};
|
|
@@ -47096,18 +47565,15 @@ var OrgDepsTag = class extends Context.Service()("@capxul/sdk/OrgDeps") {};
|
|
|
47096
47565
|
function orgDepsLayer(deps) {
|
|
47097
47566
|
return Layer.succeed(OrgDepsTag, deps);
|
|
47098
47567
|
}
|
|
47099
|
-
/**
|
|
47100
|
-
function zeroMoney() {
|
|
47101
|
-
return fromWei("0", 6, "USD");
|
|
47102
|
-
}
|
|
47103
|
-
/** A fresh Org's treasury Account: `$0` balance, `available === balance`. */
|
|
47568
|
+
/** A fresh Org's treasury Account: a zero position per settlement asset. */
|
|
47104
47569
|
function zeroTreasury(orgId) {
|
|
47105
|
-
|
|
47106
|
-
|
|
47107
|
-
|
|
47108
|
-
|
|
47109
|
-
|
|
47110
|
-
|
|
47570
|
+
return accountFromAccountPositions(toAccountId(`account_${stripPrefix(orgId)}`), CONFIGURED_MONEY_ASSETS.map((asset) => ({
|
|
47571
|
+
asset: asset.assetId,
|
|
47572
|
+
symbol: asset.symbol,
|
|
47573
|
+
decimals: asset.decimals,
|
|
47574
|
+
balance: "0",
|
|
47575
|
+
available: "0"
|
|
47576
|
+
})), void 0, MONEY_READ_RATES);
|
|
47111
47577
|
}
|
|
47112
47578
|
/** Drop a known `<prefix>_` so the remainder is reusable as another brand seed. */
|
|
47113
47579
|
function stripPrefix(value) {
|
|
@@ -47515,8 +47981,7 @@ function makeOrgMethods(deps) {
|
|
|
47515
47981
|
return {
|
|
47516
47982
|
id: treasuryResult.value.id,
|
|
47517
47983
|
address: org?.safeAddress ?? null,
|
|
47518
|
-
|
|
47519
|
-
available: treasuryResult.value.available
|
|
47984
|
+
balances: treasuryResult.value.balances
|
|
47520
47985
|
};
|
|
47521
47986
|
}), observationOnlyControls(controls), CAPXUL_OPERATIONS.org.account.get, deps.runPromise);
|
|
47522
47987
|
} },
|
|
@@ -47620,23 +48085,27 @@ const organizationPaymentExecutionUnavailable = () => Promise.resolve({
|
|
|
47620
48085
|
const holdingsContract = { current: makeFunctionReference(CAPXUL_FUNCTIONS["holdings/actions"].current) };
|
|
47621
48086
|
//#endregion
|
|
47622
48087
|
//#region ../../packages/sdk/src/domain/money/primary-holding.ts
|
|
47623
|
-
const CURRENCY_BY_TOKEN_SYMBOL = /* @__PURE__ */ new Map([["USDX", toCurrencyCode("USD")]]);
|
|
47624
48088
|
/**
|
|
47625
|
-
* The designated display asset: the
|
|
47626
|
-
* `null` when no row is
|
|
48089
|
+
* The designated display asset: the LOWEST `id` among the renderable rows, or
|
|
48090
|
+
* `null` when no row is renderable.
|
|
47627
48091
|
*
|
|
47628
|
-
* A `HoldingRow` (`packages/wire/src/money.ts`) is a RAW CHAIN ROW
|
|
47629
|
-
*
|
|
47630
|
-
*
|
|
48092
|
+
* A `HoldingRow` (`packages/wire/src/money.ts`) is a RAW CHAIN ROW. A native
|
|
48093
|
+
* row has no ERC-20 identity; a row with no `decimals` cannot be lifted into
|
|
48094
|
+
* major units; a row with no `symbol` cannot be named. Each is SKIPPED rather
|
|
48095
|
+
* than reported — a row nobody can render is not a failure of the read.
|
|
47631
48096
|
*
|
|
47632
|
-
*
|
|
47633
|
-
*
|
|
47634
|
-
*
|
|
47635
|
-
*
|
|
48097
|
+
* WHICH row is named is decided by ASSET IDENTITY — ascending `id` — never by
|
|
48098
|
+
* quantity. Two rows with different assets are not comparable money, so no
|
|
48099
|
+
* quantity can order them: 100 USDC is not "more" than 1 WETH without a rate,
|
|
48100
|
+
* and ranking major units would show whatever asset happens to have the most
|
|
48101
|
+
* decimal places as "primary". An identity order is arbitrary as well, but it
|
|
48102
|
+
* is stable and claims nothing about value. ADR-0028 R6 retires
|
|
48103
|
+
* `holdings.primary` outright; this export stays only until that retirement
|
|
48104
|
+
* lands with its remaining consumers, so no new policy is invented here.
|
|
47636
48105
|
*
|
|
47637
48106
|
* This replaces the app's hand pick in `formatCurrentHoldingsBalance`
|
|
47638
48107
|
* (`dashboard-formatters.ts`): a `.find()` over a hardcoded `["USDX","USDC"]`
|
|
47639
|
-
* list falling back to `rows[0]`.
|
|
48108
|
+
* list falling back to `rows[0]`. Duplicate ids keep the first row, so the same
|
|
47640
48109
|
* snapshot always yields the same answer.
|
|
47641
48110
|
*/
|
|
47642
48111
|
function primaryHolding(rows) {
|
|
@@ -47644,37 +48113,21 @@ function primaryHolding(rows) {
|
|
|
47644
48113
|
for (const row of rows) {
|
|
47645
48114
|
const holding = toHolding(row);
|
|
47646
48115
|
if (holding === null) continue;
|
|
47647
|
-
if (primary === null ||
|
|
48116
|
+
if (primary === null || holding.id < primary.id) primary = holding;
|
|
47648
48117
|
}
|
|
47649
48118
|
return primary;
|
|
47650
48119
|
}
|
|
47651
48120
|
function toHolding(row) {
|
|
48121
|
+
if (row.assetKind !== "erc20") return null;
|
|
47652
48122
|
if (row.symbol === void 0 || row.decimals === void 0) return null;
|
|
47653
|
-
const currency = currencyForTokenSymbol(row.symbol);
|
|
47654
|
-
if (currency === null) return null;
|
|
47655
48123
|
return {
|
|
47656
|
-
id: row.
|
|
48124
|
+
id: row.tokenAddress.toLowerCase(),
|
|
48125
|
+
assetKind: row.assetKind,
|
|
47657
48126
|
symbol: row.symbol,
|
|
47658
|
-
|
|
48127
|
+
decimals: row.decimals,
|
|
48128
|
+
value: formatUnits(BigInt(row.rawBalance), row.decimals)
|
|
47659
48129
|
};
|
|
47660
48130
|
}
|
|
47661
|
-
function currencyForTokenSymbol(symbol) {
|
|
47662
|
-
return CURRENCY_BY_TOKEN_SYMBOL.get(symbol.toUpperCase()) ?? null;
|
|
47663
|
-
}
|
|
47664
|
-
/**
|
|
47665
|
-
* Order two non-negative major-unit decimal strings without converting either
|
|
47666
|
-
* to a JavaScript number. Rows carry different `decimals`, so the raw integers
|
|
47667
|
-
* are not comparable: 1999999999 at 9 decimals is LESS than 2000000 at 6.
|
|
47668
|
-
*/
|
|
47669
|
-
function compareDecimal(first, second) {
|
|
47670
|
-
const [firstWhole = "0", firstFraction = ""] = first.split(".");
|
|
47671
|
-
const [secondWhole = "0", secondFraction = ""] = second.split(".");
|
|
47672
|
-
const width = Math.max(firstFraction.length, secondFraction.length);
|
|
47673
|
-
const firstScaled = BigInt(firstWhole + firstFraction.padEnd(width, "0"));
|
|
47674
|
-
const secondScaled = BigInt(secondWhole + secondFraction.padEnd(width, "0"));
|
|
47675
|
-
if (firstScaled === secondScaled) return 0;
|
|
47676
|
-
return firstScaled < secondScaled ? -1 : 1;
|
|
47677
|
-
}
|
|
47678
48131
|
//#endregion
|
|
47679
48132
|
//#region ../../packages/sdk/src/surface/holdings.ts
|
|
47680
48133
|
/** SPEC Freshness: past this block age the read is labelled stale. Nothing refuses. */
|
|
@@ -48123,6 +48576,7 @@ const closeClientResources = (close) => toCapxulResult(Effect.promise(close), La
|
|
|
48123
48576
|
function assembleCapxulClient(input) {
|
|
48124
48577
|
const observation = input;
|
|
48125
48578
|
const authCache = input.authCache ?? detectAuthCacheAdapter();
|
|
48579
|
+
const accountSetup = input.accountSetup ?? "automatic";
|
|
48126
48580
|
const effectRunner = input.effectRunner ?? {
|
|
48127
48581
|
runSync: Effect.runSync,
|
|
48128
48582
|
runPromise: Effect.runPromise
|
|
@@ -48193,7 +48647,7 @@ function assembleCapxulClient(input) {
|
|
|
48193
48647
|
});
|
|
48194
48648
|
kickProvisioning = accountBundle.kickProvisioning;
|
|
48195
48649
|
const account = accountBundle.methods;
|
|
48196
|
-
const
|
|
48650
|
+
const authCore = makeAuthMethods({
|
|
48197
48651
|
actor,
|
|
48198
48652
|
authClient: input.ports.authClient,
|
|
48199
48653
|
authCache,
|
|
@@ -48211,10 +48665,19 @@ function assembleCapxulClient(input) {
|
|
|
48211
48665
|
}
|
|
48212
48666
|
if (browserSigner.statusStore?.status() === "unknown" && typeof browserSigner.ensureWalletReady === "function") browserSigner.ensureWalletReady().catch(() => {});
|
|
48213
48667
|
}
|
|
48668
|
+
if (accountSetup === "explicit") return;
|
|
48214
48669
|
await detectPendingOrgInvitations?.();
|
|
48215
48670
|
kickProvisioning?.();
|
|
48216
48671
|
}
|
|
48217
48672
|
});
|
|
48673
|
+
const auth = {
|
|
48674
|
+
...authCore,
|
|
48675
|
+
clientRequests: makeClientRequestMethods({
|
|
48676
|
+
authBaseUrl: input.bootstrap.authBaseUrl,
|
|
48677
|
+
signIn: authCore.signIn,
|
|
48678
|
+
verifyOtp: authCore.verifyOtp
|
|
48679
|
+
})
|
|
48680
|
+
};
|
|
48218
48681
|
const smartAccount = makeSmartAccountMethods({
|
|
48219
48682
|
actor,
|
|
48220
48683
|
smartAccountPort: input.ports.smartAccount,
|
|
@@ -48378,6 +48841,7 @@ function assembleCapxulClient(input) {
|
|
|
48378
48841
|
...input.ports.indexerRead === void 0 ? {} : { indexer: input.ports.indexerRead },
|
|
48379
48842
|
organizationSetup: { getProofReceipt: (orgId) => runPortEffect(input.ports.convexCall.query(organizationSetupProofReceiptQuery, { orgId })) },
|
|
48380
48843
|
telemetry: input.ports.telemetry,
|
|
48844
|
+
accountSetup,
|
|
48381
48845
|
captureExternalAddressAcknowledged: (address, organizationId) => {
|
|
48382
48846
|
const telemetry = input.ports.telemetry;
|
|
48383
48847
|
if (telemetry === void 0) return;
|
|
@@ -49444,6 +49908,7 @@ async function createCapxulClientWithSignerControls(input, controls) {
|
|
|
49444
49908
|
bootstrap: adapters.value.bootstrap,
|
|
49445
49909
|
authCache: adapters.value.ports.authCache,
|
|
49446
49910
|
requirement: input.requirement ?? "none",
|
|
49911
|
+
accountSetup: input.accountSetup,
|
|
49447
49912
|
...signer === void 0 ? {} : { signer },
|
|
49448
49913
|
orgPort: Context.get(adapters.value.context, OrgPortTag),
|
|
49449
49914
|
...signer === void 0 ? {} : { organizationSetup: new ConvexOrganizationSetupAdapter({
|
|
@@ -49803,11 +50268,27 @@ function captureException(telemetry, error, context) {
|
|
|
49803
50268
|
}).pipe(Effect.flatten), () => Effect.void);
|
|
49804
50269
|
}
|
|
49805
50270
|
//#endregion
|
|
50271
|
+
//#region src/output.ts
|
|
50272
|
+
const writeStdout = (text) => Effect.flatMap(Stdio.Stdio, (stdio) => Stream.run(Stream.make(text), stdio.stdout()));
|
|
50273
|
+
const writeStderr = (text) => Effect.flatMap(Stdio.Stdio, (stdio) => Stream.run(Stream.make(text), stdio.stderr()));
|
|
50274
|
+
function renderResult(mode, envelope, lines = []) {
|
|
50275
|
+
if (mode === "json") return writeStdout(`${JSON.stringify(envelope)}\n`);
|
|
50276
|
+
return envelope.outcome === "succeeded" ? writeStdout(`${lines.join("\n")}\n`) : writeStderr(`${envelope.error.code}: ${envelope.error.message}\n`);
|
|
50277
|
+
}
|
|
50278
|
+
//#endregion
|
|
49806
50279
|
//#region src/platform/observation-identity.ts
|
|
49807
50280
|
const uuid = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u));
|
|
49808
|
-
|
|
50281
|
+
/**
|
|
50282
|
+
* One definition of the person a binding can hold.
|
|
50283
|
+
*
|
|
50284
|
+
* Exported so every CLI site that accepts or projects a verified person uses the
|
|
50285
|
+
* same rule this store enforces. A site that hand-rolls a looser check could
|
|
50286
|
+
* admit a person this store then refuses to decode, which would leave protected
|
|
50287
|
+
* state un-storable rather than merely wrong.
|
|
50288
|
+
*/
|
|
50289
|
+
const ObservationPerson = Schema.String.check(Schema.isPattern(/^[^\s]{1,256}$/u));
|
|
49809
50290
|
const binding = Schema.Struct({
|
|
49810
|
-
authUserId:
|
|
50291
|
+
authUserId: ObservationPerson,
|
|
49811
50292
|
eventId: uuid
|
|
49812
50293
|
});
|
|
49813
50294
|
const epoch = Schema.Struct({
|
|
@@ -49827,7 +50308,7 @@ const ObservationIdentityChange = Schema.Union([
|
|
|
49827
50308
|
Schema.Struct({
|
|
49828
50309
|
kind: Schema.Literal("bind"),
|
|
49829
50310
|
expectedEpoch: uuid,
|
|
49830
|
-
authUserId:
|
|
50311
|
+
authUserId: ObservationPerson
|
|
49831
50312
|
}),
|
|
49832
50313
|
Schema.Struct({
|
|
49833
50314
|
kind: Schema.Literal("rotate"),
|
|
@@ -49958,7 +50439,7 @@ const readFile = (file) => Effect.gen(function* () {
|
|
|
49958
50439
|
});
|
|
49959
50440
|
const storageError = (error) => error._tag === "StateFileError" ? error : fail("Io");
|
|
49960
50441
|
const decodeSettings = Schema.decodeUnknownEffect(Schema.fromJsonString(Settings), { onExcessProperty: "error" });
|
|
49961
|
-
Schema.decodeUnknownEffect(Schema.fromJsonString(PrivateRecord), { onExcessProperty: "error" });
|
|
50442
|
+
const decodeRecord = Schema.decodeUnknownEffect(Schema.fromJsonString(PrivateRecord), { onExcessProperty: "error" });
|
|
49962
50443
|
const readSettings = (file) => Effect.gen(function* () {
|
|
49963
50444
|
const location = yield* locate(file, false).pipe(Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail(error)));
|
|
49964
50445
|
if (location === null) return null;
|
|
@@ -50054,11 +50535,59 @@ const setCollectionEnabled = (file, enabled) => Effect.gen(function* () {
|
|
|
50054
50535
|
})));
|
|
50055
50536
|
return settings;
|
|
50056
50537
|
}).pipe(Effect.mapError(storageError));
|
|
50057
|
-
const
|
|
50538
|
+
const recordLocation = (directory, scope) => Effect.gen(function* () {
|
|
50539
|
+
const path = yield* Path.Path;
|
|
50540
|
+
const crypto = yield* Crypto.Crypto;
|
|
50541
|
+
const valid = yield* Schema.decodeUnknownEffect(PrivateRecordScope, { onExcessProperty: "error" })(scope).pipe(Effect.mapError(() => fail("Corrupt")));
|
|
50542
|
+
const encoded = JSON.stringify([
|
|
50543
|
+
valid.application,
|
|
50544
|
+
valid.issuer,
|
|
50545
|
+
valid.environment,
|
|
50546
|
+
valid.person
|
|
50547
|
+
]);
|
|
50548
|
+
const hash = yield* crypto.digest("SHA-256", new TextEncoder().encode(encoded));
|
|
50549
|
+
return path.join(directory, `${Array.from(hash, (byte) => byte.toString(16).padStart(2, "0")).join("")}.json`);
|
|
50550
|
+
});
|
|
50551
|
+
const readPrivateRecord = (directory, scope) => Effect.gen(function* () {
|
|
50552
|
+
const file = yield* recordLocation(directory, scope);
|
|
50553
|
+
const location = yield* locate(file, false).pipe(Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail(error)));
|
|
50554
|
+
if (location === null) return null;
|
|
50555
|
+
const content = yield* readFile(location);
|
|
50556
|
+
if (content === null) return null;
|
|
50557
|
+
const record = yield* decodeRecord(content).pipe(Effect.mapError(() => fail("Corrupt")));
|
|
50558
|
+
if (Object.keys(scope).some((key) => Reflect.get(scope, key) !== Reflect.get(record.scope, key))) return yield* Effect.fail(fail("Corrupt"));
|
|
50559
|
+
return record.value;
|
|
50560
|
+
}).pipe(Effect.mapError(storageError));
|
|
50561
|
+
const writePrivateRecord = (directory, scope, value) => Effect.gen(function* () {
|
|
50562
|
+
const file = yield* recordLocation(directory, scope);
|
|
50563
|
+
const location = yield* locate(file, true);
|
|
50564
|
+
yield* replace(location, readPrivateRecord(directory, scope).pipe(Effect.as({
|
|
50565
|
+
content: JSON.stringify({
|
|
50566
|
+
version: 1,
|
|
50567
|
+
scope,
|
|
50568
|
+
value
|
|
50569
|
+
}),
|
|
50570
|
+
value: void 0
|
|
50571
|
+
})));
|
|
50572
|
+
}).pipe(Effect.mapError(storageError));
|
|
50573
|
+
/**
|
|
50574
|
+
* `confirm` is evaluated under this record's lock, so it cannot be separated
|
|
50575
|
+
* from the write it gates. A guard checked before the lock is only a snapshot:
|
|
50576
|
+
* by the time the transition runs, the world it inspected can have changed.
|
|
50577
|
+
*
|
|
50578
|
+
* Its error channel is `never` on purpose. A guard that cannot answer must mean
|
|
50579
|
+
* "refuse the change", never "fail the operation" — and refusing returns `stale`,
|
|
50580
|
+
* the answer an epoch CAS already returns, so callers handle both alike.
|
|
50581
|
+
*/
|
|
50582
|
+
const changeObservationIdentity = (directory, input, confirm = Effect.succeed(true)) => Effect.gen(function* () {
|
|
50058
50583
|
const change = yield* Schema.decodeUnknownEffect(ObservationIdentityChange, { onExcessProperty: "error" })(input).pipe(Effect.mapError(() => fail("Corrupt")));
|
|
50059
50584
|
const path = yield* Path.Path;
|
|
50060
50585
|
const location = yield* locate(path.join(directory, "observation.json"), true);
|
|
50061
50586
|
return yield* replace(location, Effect.gen(function* () {
|
|
50587
|
+
if (!(yield* confirm)) return {
|
|
50588
|
+
content: null,
|
|
50589
|
+
value: { status: "stale" }
|
|
50590
|
+
};
|
|
50062
50591
|
const content = yield* readFile(location);
|
|
50063
50592
|
const previous = content === null ? null : yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ObservationIdentity), { onExcessProperty: "error" })(content).pipe(Effect.mapError(() => fail("Corrupt")));
|
|
50064
50593
|
const crypto = yield* Crypto.Crypto;
|
|
@@ -50073,64 +50602,104 @@ const changeObservationIdentity = (directory, input) => Effect.gen(function* ()
|
|
|
50073
50602
|
};
|
|
50074
50603
|
}));
|
|
50075
50604
|
}).pipe(Effect.mapError(storageError));
|
|
50076
|
-
/**
|
|
50077
|
-
|
|
50078
|
-
|
|
50079
|
-
|
|
50080
|
-
|
|
50081
|
-
|
|
50082
|
-
|
|
50083
|
-
|
|
50084
|
-
|
|
50085
|
-
|
|
50086
|
-
|
|
50087
|
-
|
|
50088
|
-
|
|
50089
|
-
|
|
50090
|
-
|
|
50091
|
-
|
|
50092
|
-
|
|
50093
|
-
|
|
50094
|
-
|
|
50095
|
-
|
|
50096
|
-
|
|
50097
|
-
|
|
50098
|
-
|
|
50099
|
-
|
|
50100
|
-
|
|
50101
|
-
|
|
50102
|
-
|
|
50103
|
-
|
|
50104
|
-
const
|
|
50105
|
-
const
|
|
50106
|
-
|
|
50107
|
-
|
|
50108
|
-
|
|
50109
|
-
|
|
50110
|
-
|
|
50111
|
-
|
|
50112
|
-
|
|
50113
|
-
}
|
|
50114
|
-
|
|
50115
|
-
|
|
50116
|
-
|
|
50117
|
-
|
|
50118
|
-
|
|
50119
|
-
|
|
50120
|
-
|
|
50121
|
-
|
|
50122
|
-
|
|
50123
|
-
|
|
50124
|
-
|
|
50125
|
-
|
|
50126
|
-
|
|
50127
|
-
|
|
50128
|
-
|
|
50129
|
-
|
|
50130
|
-
|
|
50131
|
-
|
|
50132
|
-
|
|
50133
|
-
|
|
50605
|
+
/**
|
|
50606
|
+
* The invocation-start snapshot: one locked read that acquires an epoch only when
|
|
50607
|
+
* none is bound.
|
|
50608
|
+
*
|
|
50609
|
+
* `acquire` rotates a bound epoch by design, because an anonymous caller must
|
|
50610
|
+
* never inherit a person's ID. An ordinary invocation from an already signed-in
|
|
50611
|
+
* person is not that transition, so it must not rotate — otherwise every command
|
|
50612
|
+
* run after login would unbind them. Deciding between the two needs the lock:
|
|
50613
|
+
* a pre-lock snapshot must never decide an identity binding.
|
|
50614
|
+
*
|
|
50615
|
+
* This reads the stored binding for observation bookkeeping only. It does not
|
|
50616
|
+
* make the person authenticated and never re-verifies a grant.
|
|
50617
|
+
*/
|
|
50618
|
+
const snapshotInvocationIdentity = (directory) => Effect.gen(function* () {
|
|
50619
|
+
const path = yield* Path.Path;
|
|
50620
|
+
const location = yield* locate(path.join(directory, "observation.json"), true);
|
|
50621
|
+
return yield* replace(location, Effect.gen(function* () {
|
|
50622
|
+
const content = yield* readFile(location);
|
|
50623
|
+
const previous = content === null ? null : yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ObservationIdentity), { onExcessProperty: "error" })(content).pipe(Effect.mapError(() => fail("Corrupt")));
|
|
50624
|
+
if (previous !== null && previous.epoch.binding !== null) return {
|
|
50625
|
+
content: null,
|
|
50626
|
+
value: {
|
|
50627
|
+
status: "ready",
|
|
50628
|
+
firstUse: previous.firstUse,
|
|
50629
|
+
firstUseClaimed: false,
|
|
50630
|
+
epoch: previous.epoch
|
|
50631
|
+
}
|
|
50632
|
+
};
|
|
50633
|
+
const crypto = yield* Crypto.Crypto;
|
|
50634
|
+
const next = transitionObservationIdentity(previous, { kind: "acquire" }, {
|
|
50635
|
+
epochId: yield* crypto.randomUUIDv4,
|
|
50636
|
+
eventId: yield* crypto.randomUUIDv4,
|
|
50637
|
+
observedAt: yield* Clock.currentTimeMillis
|
|
50638
|
+
});
|
|
50639
|
+
return {
|
|
50640
|
+
content: next.state === previous ? null : JSON.stringify(next.state),
|
|
50641
|
+
value: next.result
|
|
50642
|
+
};
|
|
50643
|
+
}));
|
|
50644
|
+
}).pipe(Effect.mapError(storageError));
|
|
50645
|
+
/**
|
|
50646
|
+
* The supplied person is verified by AUTH, not by this observation store.
|
|
50647
|
+
*
|
|
50648
|
+
* `confirm` lets AUTH re-check its own facts under this record's lock, which is
|
|
50649
|
+
* the only place a sign-in and a sign-out are guaranteed to serialize. The epoch
|
|
50650
|
+
* CAS alone cannot close that race: a sign-out that finds the epoch still
|
|
50651
|
+
* anonymous leaves it byte-identical rather than rotating it, so an epoch
|
|
50652
|
+
* snapshotted before a network call is still the expected one afterwards.
|
|
50653
|
+
*/
|
|
50654
|
+
const bindObservationIdentity = (directory, expectedEpoch, authUserId, confirm = Effect.succeed(true)) => changeObservationIdentity(directory, {
|
|
50655
|
+
kind: "bind",
|
|
50656
|
+
expectedEpoch,
|
|
50657
|
+
authUserId
|
|
50658
|
+
}, confirm);
|
|
50659
|
+
/**
|
|
50660
|
+
* Drop the person binding at sign-out. Reports whether anything was cleared.
|
|
50661
|
+
*
|
|
50662
|
+
* A rotate has to name the epoch it expects to replace. Reading that epoch and
|
|
50663
|
+
* rotating it in a second locked step leaves a gap: a concurrent sign-in can bind
|
|
50664
|
+
* a person in between, and the rotate then refuses the changed epoch, so the
|
|
50665
|
+
* sign-out quietly leaves a person bound. Reading and rotating under one lock
|
|
50666
|
+
* closes the gap, which is why this is not `rotateObservationIdentity` plus a
|
|
50667
|
+
* snapshot.
|
|
50668
|
+
*
|
|
50669
|
+
* Unlike every acquire path here, this never creates observation state. A user
|
|
50670
|
+
* who declined collection has no observation file, and signing out must not hand
|
|
50671
|
+
* them one — or a directory to put it in.
|
|
50672
|
+
*/
|
|
50673
|
+
const clearObservationBinding = (directory) => Effect.gen(function* () {
|
|
50674
|
+
const path = yield* Path.Path;
|
|
50675
|
+
const location = yield* locate(path.join(directory, "observation.json"), false).pipe(Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail(error)));
|
|
50676
|
+
if (location === null) return false;
|
|
50677
|
+
return yield* replace(location, Effect.gen(function* () {
|
|
50678
|
+
const content = yield* readFile(location);
|
|
50679
|
+
if (content === null) return {
|
|
50680
|
+
content: null,
|
|
50681
|
+
value: false
|
|
50682
|
+
};
|
|
50683
|
+
const previous = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ObservationIdentity), { onExcessProperty: "error" })(content).pipe(Effect.mapError(() => fail("Corrupt")));
|
|
50684
|
+
if (previous.epoch.binding === null) return {
|
|
50685
|
+
content: null,
|
|
50686
|
+
value: false
|
|
50687
|
+
};
|
|
50688
|
+
const crypto = yield* Crypto.Crypto;
|
|
50689
|
+
const next = transitionObservationIdentity(previous, {
|
|
50690
|
+
kind: "rotate",
|
|
50691
|
+
expectedEpoch: previous.epoch.id
|
|
50692
|
+
}, {
|
|
50693
|
+
epochId: yield* crypto.randomUUIDv4,
|
|
50694
|
+
eventId: yield* crypto.randomUUIDv4,
|
|
50695
|
+
observedAt: yield* Clock.currentTimeMillis
|
|
50696
|
+
});
|
|
50697
|
+
return {
|
|
50698
|
+
content: next.state === previous ? null : JSON.stringify(next.state),
|
|
50699
|
+
value: next.result.status === "ready"
|
|
50700
|
+
};
|
|
50701
|
+
}));
|
|
50702
|
+
}).pipe(Effect.mapError(storageError));
|
|
50134
50703
|
//#endregion
|
|
50135
50704
|
//#region src/runtime/errors.ts
|
|
50136
50705
|
var CliFailure = class extends Data.TaggedError("CliFailure") {};
|
|
@@ -50183,74 +50752,396 @@ function parsedCommand(cause) {
|
|
|
50183
50752
|
return CliError.isCliError(error) && error._tag === "ShowHelp" ? error.commandPath.slice(1).join(".") || "capxul" : "capxul";
|
|
50184
50753
|
}
|
|
50185
50754
|
//#endregion
|
|
50186
|
-
//#region src/
|
|
50187
|
-
const
|
|
50188
|
-
|
|
50189
|
-
const
|
|
50190
|
-
function
|
|
50191
|
-
return
|
|
50192
|
-
|
|
50193
|
-
|
|
50194
|
-
|
|
50195
|
-
|
|
50196
|
-
|
|
50197
|
-
|
|
50198
|
-
|
|
50199
|
-
|
|
50200
|
-
|
|
50201
|
-
}
|
|
50202
|
-
|
|
50203
|
-
const
|
|
50204
|
-
const
|
|
50205
|
-
|
|
50206
|
-
|
|
50207
|
-
|
|
50208
|
-
|
|
50209
|
-
token: Config.option(Config.redacted("CAPXUL_POSTHOG_PROJECT_TOKEN")),
|
|
50210
|
-
disabled: Config.boolean("CAPXUL_TELEMETRY_DISABLED").pipe(Config.withDefault(false))
|
|
50211
|
-
}).pipe(Effect.mapError(() => configurationFailure("CLI environment configuration is invalid.")));
|
|
50212
|
-
let directory = Option.getOrUndefined(values.cliHome);
|
|
50213
|
-
if (directory === void 0) {
|
|
50214
|
-
const configHome = Option.getOrUndefined(values.xdg);
|
|
50215
|
-
const home = Option.getOrUndefined(values.home);
|
|
50216
|
-
if (configHome !== void 0) directory = path.join(configHome, "capxul", "cli");
|
|
50217
|
-
else if (home !== void 0) directory = path.join(home, ".config", "capxul", "cli");
|
|
50218
|
-
else return yield* Effect.fail(configurationFailure("Set HOME or CAPXUL_CLI_HOME."));
|
|
50219
|
-
}
|
|
50220
|
-
const settingsPath = path.join(path.resolve(directory), "settings.json");
|
|
50221
|
-
const host = Option.getOrUndefined(values.host) ?? defaultHost;
|
|
50755
|
+
//#region src/auth/native-http.ts
|
|
50756
|
+
const NATIVE_CLIENT_ID = "capxul-cli";
|
|
50757
|
+
var NativeHttpError = class extends Data.TaggedError("NativeHttpError") {};
|
|
50758
|
+
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
50759
|
+
function originPath(origin, path) {
|
|
50760
|
+
return `${origin.replace(/\/$/, "")}/api/auth/native${path}`;
|
|
50761
|
+
}
|
|
50762
|
+
async function readJson(response) {
|
|
50763
|
+
return response.json().catch(() => null);
|
|
50764
|
+
}
|
|
50765
|
+
function asNonEmptyString(value) {
|
|
50766
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
50767
|
+
}
|
|
50768
|
+
function asFiniteNumber(value) {
|
|
50769
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
50770
|
+
}
|
|
50771
|
+
function decodeStarted(body) {
|
|
50772
|
+
const requestId = asNonEmptyString(body.requestId);
|
|
50773
|
+
const deviceCode = asNonEmptyString(body.device_code);
|
|
50774
|
+
const verificationUri = asNonEmptyString(body.verification_uri);
|
|
50775
|
+
const expiresIn = asFiniteNumber(body.expires_in);
|
|
50776
|
+
const interval = asFiniteNumber(body.interval);
|
|
50777
|
+
if (requestId === null || deviceCode === null || verificationUri === null || expiresIn === null || expiresIn < 0 || interval === null || interval < 0) return null;
|
|
50222
50778
|
return {
|
|
50223
|
-
|
|
50224
|
-
|
|
50225
|
-
|
|
50226
|
-
|
|
50227
|
-
|
|
50228
|
-
|
|
50779
|
+
requestId,
|
|
50780
|
+
deviceCode,
|
|
50781
|
+
verificationUri,
|
|
50782
|
+
expiresIn,
|
|
50783
|
+
interval
|
|
50784
|
+
};
|
|
50785
|
+
}
|
|
50786
|
+
function decodeGrant(body) {
|
|
50787
|
+
const grantId = asNonEmptyString(body.grantId);
|
|
50788
|
+
const email = asNonEmptyString(body.email);
|
|
50789
|
+
const accessToken = asNonEmptyString(body.access_token);
|
|
50790
|
+
const refreshToken = asNonEmptyString(body.refresh_token);
|
|
50791
|
+
const revision = asFiniteNumber(body.revision);
|
|
50792
|
+
const accessExpiresAt = asFiniteNumber(body.accessExpiresAt);
|
|
50793
|
+
const renewalExpiresAt = asFiniteNumber(body.renewalExpiresAt);
|
|
50794
|
+
const absoluteExpiresAt = asFiniteNumber(body.absoluteExpiresAt);
|
|
50795
|
+
if (grantId === null || email === null || accessToken === null || refreshToken === null || revision === null || accessExpiresAt === null || renewalExpiresAt === null || absoluteExpiresAt === null) return null;
|
|
50796
|
+
return {
|
|
50797
|
+
grantId,
|
|
50798
|
+
revision,
|
|
50799
|
+
email,
|
|
50800
|
+
accessToken,
|
|
50801
|
+
refreshToken,
|
|
50802
|
+
accessExpiresAt,
|
|
50803
|
+
renewalExpiresAt,
|
|
50804
|
+
absoluteExpiresAt
|
|
50229
50805
|
};
|
|
50806
|
+
}
|
|
50807
|
+
const startNativeRequest = (input) => Effect.tryPromise({
|
|
50808
|
+
try: async () => {
|
|
50809
|
+
const response = await fetch(originPath(input.origin, "/device/code"), {
|
|
50810
|
+
method: "POST",
|
|
50811
|
+
headers: { "content-type": "application/json" },
|
|
50812
|
+
body: JSON.stringify({
|
|
50813
|
+
clientId: NATIVE_CLIENT_ID,
|
|
50814
|
+
email: input.email,
|
|
50815
|
+
intent: input.intent,
|
|
50816
|
+
waitMs: input.waitMs
|
|
50817
|
+
}),
|
|
50818
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
50819
|
+
});
|
|
50820
|
+
const body = await readJson(response);
|
|
50821
|
+
const started = body === null ? null : decodeStarted(body);
|
|
50822
|
+
if (!response.ok || started === null) throw new NativeHttpError({
|
|
50823
|
+
reason: (typeof body?.error === "string" ? body.error : "invalid_request") === "slow_down" ? "slow_down" : "invalid_request",
|
|
50824
|
+
...typeof body?.retry_after === "number" ? { retryAfterMs: body.retry_after * 1e3 } : {}
|
|
50825
|
+
});
|
|
50826
|
+
return started;
|
|
50827
|
+
},
|
|
50828
|
+
catch: (cause) => cause instanceof NativeHttpError ? cause : new NativeHttpError({ reason: "network" })
|
|
50230
50829
|
});
|
|
50231
|
-
const
|
|
50232
|
-
|
|
50233
|
-
|
|
50234
|
-
|
|
50235
|
-
|
|
50236
|
-
|
|
50237
|
-
|
|
50238
|
-
|
|
50239
|
-
|
|
50240
|
-
|
|
50241
|
-
|
|
50830
|
+
const pollNativeToken = (input) => Effect.tryPromise({
|
|
50831
|
+
try: async () => {
|
|
50832
|
+
const response = await fetch(originPath(input.origin, "/token"), {
|
|
50833
|
+
method: "POST",
|
|
50834
|
+
headers: { "content-type": "application/json" },
|
|
50835
|
+
body: JSON.stringify({
|
|
50836
|
+
client_id: NATIVE_CLIENT_ID,
|
|
50837
|
+
grant_type: DEVICE_GRANT,
|
|
50838
|
+
device_code: input.deviceCode
|
|
50839
|
+
}),
|
|
50840
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
50841
|
+
});
|
|
50842
|
+
const body = await readJson(response);
|
|
50843
|
+
if (response.status === 400 && body?.error === "authorization_pending") throw new NativeHttpError({ reason: "authorization_pending" });
|
|
50844
|
+
if (response.status === 429 || body?.error === "slow_down") throw new NativeHttpError({
|
|
50845
|
+
reason: "slow_down",
|
|
50846
|
+
retryAfterMs: typeof body?.retry_after === "number" ? body.retry_after * 1e3 : 5e3
|
|
50847
|
+
});
|
|
50848
|
+
const grant = body === null ? null : decodeGrant(body);
|
|
50849
|
+
if (!response.ok || grant === null) {
|
|
50850
|
+
const error = typeof body?.error === "string" ? body.error : "invalid_request";
|
|
50851
|
+
throw new NativeHttpError({ reason: error === "access_denied" || error === "expired_token" ? error : "invalid_request" });
|
|
50852
|
+
}
|
|
50853
|
+
return grant;
|
|
50854
|
+
},
|
|
50855
|
+
catch: (cause) => cause instanceof NativeHttpError ? cause : new NativeHttpError({ reason: "network" })
|
|
50856
|
+
});
|
|
50857
|
+
/**
|
|
50858
|
+
* The protected store's own rule, so anything accepted here can also be stored.
|
|
50859
|
+
* Returns the validated person rather than a boolean, so the caller cannot
|
|
50860
|
+
* forward a value that only passed a shape check.
|
|
50861
|
+
*/
|
|
50862
|
+
const verifiedPerson = (value) => Schema.decodeUnknownOption(ObservationPerson)(value);
|
|
50863
|
+
/**
|
|
50864
|
+
* Ask the issuer which verified person a stored access token belongs to.
|
|
50865
|
+
*
|
|
50866
|
+
* Observation never decodes a token to invent authority and never uses an email
|
|
50867
|
+
* as a person. `authUserId` therefore comes from the issuer alone: it re-verifies
|
|
50868
|
+
* the JWT and reads the grant row. The active response also carries the email,
|
|
50869
|
+
* account, application, resource and scope — this decode keeps the subject and
|
|
50870
|
+
* drops all of it.
|
|
50871
|
+
*
|
|
50872
|
+
* An inactive grant is a valid answer rather than a transport failure, so callers
|
|
50873
|
+
* can treat `{active: false}` as "stay anonymous" without error handling.
|
|
50874
|
+
*/
|
|
50875
|
+
const resolveNativeSession = (input) => Effect.tryPromise({
|
|
50876
|
+
try: async () => {
|
|
50877
|
+
const response = await fetch(originPath(input.origin, "/session"), {
|
|
50878
|
+
method: "POST",
|
|
50879
|
+
headers: { "content-type": "application/json" },
|
|
50880
|
+
body: JSON.stringify({
|
|
50881
|
+
client_id: NATIVE_CLIENT_ID,
|
|
50882
|
+
token: input.accessToken
|
|
50883
|
+
}),
|
|
50884
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
50885
|
+
});
|
|
50886
|
+
const body = await readJson(response);
|
|
50887
|
+
if (response.status === 503) throw new NativeHttpError({ reason: "network" });
|
|
50888
|
+
if (!response.ok || body === null) {
|
|
50889
|
+
const error = typeof body?.error === "string" ? body.error : "invalid_request";
|
|
50890
|
+
throw new NativeHttpError({ reason: error === "access_denied" || error === "expired_token" ? error : "invalid_request" });
|
|
50891
|
+
}
|
|
50892
|
+
if (body.active === false) return { active: false };
|
|
50893
|
+
if (body.active !== true) throw new NativeHttpError({ reason: "invalid_request" });
|
|
50894
|
+
const authUserId = verifiedPerson(body.authUserId);
|
|
50895
|
+
if (Option.isNone(authUserId)) throw new NativeHttpError({ reason: "invalid_request" });
|
|
50896
|
+
return {
|
|
50897
|
+
active: true,
|
|
50898
|
+
authUserId: authUserId.value
|
|
50899
|
+
};
|
|
50900
|
+
},
|
|
50901
|
+
catch: (cause) => cause instanceof NativeHttpError ? cause : new NativeHttpError({ reason: "network" })
|
|
50902
|
+
});
|
|
50903
|
+
const revokeNativeGrant = (input) => Effect.tryPromise({
|
|
50904
|
+
try: async () => {
|
|
50905
|
+
if (!(await fetch(originPath(input.origin, "/revoke"), {
|
|
50906
|
+
method: "POST",
|
|
50907
|
+
headers: { "content-type": "application/json" },
|
|
50908
|
+
body: JSON.stringify({
|
|
50909
|
+
client_id: NATIVE_CLIENT_ID,
|
|
50910
|
+
token: input.token,
|
|
50911
|
+
token_type_hint: input.hint
|
|
50912
|
+
}),
|
|
50913
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
50914
|
+
})).ok) throw new NativeHttpError({ reason: "network" });
|
|
50915
|
+
},
|
|
50916
|
+
catch: (cause) => cause instanceof NativeHttpError ? cause : new NativeHttpError({ reason: "network" })
|
|
50917
|
+
});
|
|
50918
|
+
//#endregion
|
|
50919
|
+
//#region src/auth/session.ts
|
|
50920
|
+
const StoredGrant = Schema.Struct({
|
|
50921
|
+
version: Schema.Literal(1),
|
|
50922
|
+
tombstone: Schema.optional(Schema.Boolean),
|
|
50923
|
+
grantId: Schema.optional(Schema.String),
|
|
50924
|
+
revision: Schema.optional(Schema.Number),
|
|
50925
|
+
email: Schema.optional(Schema.String),
|
|
50926
|
+
accessToken: Schema.optional(Schema.String),
|
|
50927
|
+
refreshToken: Schema.optional(Schema.String),
|
|
50928
|
+
accessExpiresAt: Schema.optional(Schema.Number),
|
|
50929
|
+
renewalExpiresAt: Schema.optional(Schema.Number),
|
|
50930
|
+
absoluteExpiresAt: Schema.optional(Schema.Number)
|
|
50931
|
+
});
|
|
50932
|
+
const scopeFor = (config, person) => ({
|
|
50933
|
+
application: "capxul-cli",
|
|
50934
|
+
issuer: config.bootstrapBaseUrl,
|
|
50935
|
+
environment: config.environment,
|
|
50936
|
+
person
|
|
50937
|
+
});
|
|
50938
|
+
const recordsDir = (config) => Effect.map(Path.Path, (path) => path.join(path.dirname(config.settingsPath), "private"));
|
|
50939
|
+
const loadGrant = (config, email) => Effect.gen(function* () {
|
|
50940
|
+
const directory = yield* recordsDir(config);
|
|
50941
|
+
const value = yield* readPrivateRecord(directory, scopeFor(config, email.toLowerCase()));
|
|
50942
|
+
if (value === null) return null;
|
|
50943
|
+
return yield* Schema.decodeUnknownEffect(StoredGrant, { onExcessProperty: "error" })(JSON.parse(value));
|
|
50944
|
+
});
|
|
50945
|
+
const saveGrant = (config, grant) => Effect.gen(function* () {
|
|
50946
|
+
const directory = yield* recordsDir(config);
|
|
50947
|
+
const stored = {
|
|
50948
|
+
version: 1,
|
|
50949
|
+
grantId: grant.grantId,
|
|
50950
|
+
revision: grant.revision,
|
|
50951
|
+
email: grant.email,
|
|
50952
|
+
accessToken: grant.accessToken,
|
|
50953
|
+
refreshToken: grant.refreshToken,
|
|
50954
|
+
accessExpiresAt: grant.accessExpiresAt,
|
|
50955
|
+
renewalExpiresAt: grant.renewalExpiresAt,
|
|
50956
|
+
absoluteExpiresAt: grant.absoluteExpiresAt
|
|
50957
|
+
};
|
|
50958
|
+
yield* writePrivateRecord(directory, scopeFor(config, grant.email.toLowerCase()), JSON.stringify(stored));
|
|
50959
|
+
});
|
|
50960
|
+
const tombstoneGrant = (config, email) => Effect.gen(function* () {
|
|
50961
|
+
const directory = yield* recordsDir(config);
|
|
50962
|
+
const previous = yield* loadGrant(config, email);
|
|
50963
|
+
const stored = {
|
|
50964
|
+
version: 1,
|
|
50965
|
+
tombstone: true,
|
|
50966
|
+
email: email.toLowerCase(),
|
|
50967
|
+
grantId: previous?.grantId
|
|
50242
50968
|
};
|
|
50969
|
+
yield* writePrivateRecord(directory, scopeFor(config, email.toLowerCase()), JSON.stringify(stored));
|
|
50970
|
+
return previous;
|
|
50243
50971
|
});
|
|
50244
|
-
const
|
|
50245
|
-
|
|
50246
|
-
|
|
50247
|
-
|
|
50972
|
+
const storedGrantSecrets = (stored) => stored !== null && stored.tombstone !== true && typeof stored.grantId === "string" && stored.grantId.length > 0 && typeof stored.revision === "number" && Number.isFinite(stored.revision) && typeof stored.email === "string" && stored.email.length > 0 && typeof stored.accessToken === "string" && stored.accessToken.length > 0 && typeof stored.refreshToken === "string" && stored.refreshToken.length > 0 && typeof stored.accessExpiresAt === "number" && Number.isFinite(stored.accessExpiresAt) && typeof stored.renewalExpiresAt === "number" && Number.isFinite(stored.renewalExpiresAt) && typeof stored.absoluteExpiresAt === "number" && Number.isFinite(stored.absoluteExpiresAt);
|
|
50973
|
+
const usableGrant = (stored, now = Date.now()) => storedGrantSecrets(stored) && now < stored.renewalExpiresAt && now < stored.absoluteExpiresAt;
|
|
50974
|
+
//#endregion
|
|
50975
|
+
//#region src/auth/workflow.ts
|
|
50976
|
+
const missingEmail = new CliFailure({
|
|
50977
|
+
code: "CLI_USAGE",
|
|
50978
|
+
message: "Provide --email. Non-interactive runs cannot prompt.",
|
|
50979
|
+
exitCode: 2,
|
|
50980
|
+
outcome: "refused"
|
|
50981
|
+
});
|
|
50982
|
+
const toFailure = (error) => new CliFailure({
|
|
50983
|
+
code: error.reason === "access_denied" || error.reason === "expired_token" ? "NOT_AUTHENTICATED" : error.reason === "network" ? "NETWORK_ERROR" : "CLI_USAGE",
|
|
50984
|
+
message: error.reason === "access_denied" ? "The request was denied." : error.reason === "expired_token" ? "The request expired before it was approved." : error.reason === "network" ? "The Capxul issuer could not be reached." : "The native authorization request was refused.",
|
|
50985
|
+
exitCode: error.reason === "network" ? 5 : 3,
|
|
50986
|
+
outcome: "refused"
|
|
50987
|
+
});
|
|
50988
|
+
const pollUntilGranted = (origin, deviceCode, intervalSeconds) => pollNativeToken({
|
|
50989
|
+
origin,
|
|
50990
|
+
deviceCode
|
|
50991
|
+
}).pipe(Effect.catchTag("NativeHttpError", (error) => {
|
|
50992
|
+
if (error.reason === "authorization_pending" || error.reason === "slow_down") return pollUntilGranted(origin, deviceCode, intervalSeconds).pipe(Effect.delay(Duration.millis(error.retryAfterMs ?? Math.max(intervalSeconds * 1e3, 1))));
|
|
50993
|
+
return Effect.fail(toFailure(error));
|
|
50994
|
+
}));
|
|
50995
|
+
/** The same eligibility gate invocation-start observation uses. */
|
|
50996
|
+
const observationEligible = (config) => config.posthogHost !== void 0 && config.projectToken !== void 0 && config.collectionEnabled();
|
|
50997
|
+
/**
|
|
50998
|
+
* The deadline for asking the issuer who a grant belongs to.
|
|
50999
|
+
*
|
|
51000
|
+
* Short by design: this runs after the grant is stored, so a person is signed in
|
|
51001
|
+
* whether or not it answers. It bounds observation bookkeeping, not an operation
|
|
51002
|
+
* the user is waiting on, so it is deliberately far below the online diagnostic
|
|
51003
|
+
* deadline. A missed deadline costs one anonymous epoch, nothing else.
|
|
51004
|
+
*/
|
|
51005
|
+
const OBSERVATION_BIND_TIMEOUT_MS = 5e3;
|
|
51006
|
+
/**
|
|
51007
|
+
* Whether the grant this bind was resolved for is still the signed-in one.
|
|
51008
|
+
*
|
|
51009
|
+
* Evaluated by the observation store under its own record lock, which is the
|
|
51010
|
+
* only place a sign-in and a sign-out serialize. The epoch CAS cannot substitute
|
|
51011
|
+
* for this: signing out of an epoch that is still anonymous leaves it
|
|
51012
|
+
* byte-identical rather than rotating it, so an epoch snapshotted before the
|
|
51013
|
+
* person lookup is still the expected one after a sign-out completed during it —
|
|
51014
|
+
* and the bind would persist a person who is no longer signed in.
|
|
51015
|
+
*
|
|
51016
|
+
* The comparison is the one `refreshIfDue` already makes before trusting a
|
|
51017
|
+
* rotated grant. An unreadable record answers `false`: a guard that cannot
|
|
51018
|
+
* confirm must refuse the bind, never fail the sign-in.
|
|
51019
|
+
*/
|
|
51020
|
+
const grantStillCurrent = (config, grant) => loadGrant(config, grant.email).pipe(Effect.map((stored) => storedGrantSecrets(stored) && stored.grantId === grant.grantId && stored.revision === grant.revision && stored.refreshToken === grant.refreshToken), Effect.catch(() => Effect.succeed(false)));
|
|
51021
|
+
/**
|
|
51022
|
+
* Connect this device's eligible anonymous epoch to the person AUTH verified.
|
|
51023
|
+
*
|
|
51024
|
+
* The person comes from the issuer's session endpoint, never from the stored
|
|
51025
|
+
* grant: observation does not decode a token to invent authority and does not
|
|
51026
|
+
* use an email as a person. The grant supplies only the credential to present.
|
|
51027
|
+
*
|
|
51028
|
+
* Best effort in both directions. An unreachable issuer, an inactive grant or a
|
|
51029
|
+
* lost binding race leaves the person signed in and the epoch anonymous, so
|
|
51030
|
+
* observation can never fail or delay an AUTH transition.
|
|
51031
|
+
*/
|
|
51032
|
+
const bindObservationToPerson = (config, grant) => Effect.gen(function* () {
|
|
51033
|
+
if (!observationEligible(config)) return;
|
|
51034
|
+
const directory = (yield* Path.Path).dirname(config.settingsPath);
|
|
51035
|
+
const snapshot = yield* snapshotInvocationIdentity(directory);
|
|
51036
|
+
if (snapshot.status !== "ready") return;
|
|
51037
|
+
const session = yield* resolveNativeSession({
|
|
51038
|
+
origin: config.bootstrapBaseUrl,
|
|
51039
|
+
accessToken: grant.accessToken,
|
|
51040
|
+
signal: AbortSignal.timeout(OBSERVATION_BIND_TIMEOUT_MS)
|
|
51041
|
+
}).pipe(Effect.timeoutOption(Duration.millis(OBSERVATION_BIND_TIMEOUT_MS)));
|
|
51042
|
+
if (Option.isNone(session)) return;
|
|
51043
|
+
if (!session.value.active || session.value.authUserId === void 0) return;
|
|
51044
|
+
yield* bindObservationIdentity(directory, snapshot.epoch.id, session.value.authUserId, grantStillCurrent(config, grant));
|
|
51045
|
+
}).pipe(Effect.ignore);
|
|
51046
|
+
/**
|
|
51047
|
+
* Disconnect observation at the approved sign-out transition.
|
|
51048
|
+
*
|
|
51049
|
+
* Rotating replaces the bound epoch with a fresh anonymous one, so later
|
|
51050
|
+
* invocations project as anonymous again. It never touches the collection
|
|
51051
|
+
* setting: AUTH sign-out does not re-enable collection.
|
|
51052
|
+
*
|
|
51053
|
+
* Not gated on collection eligibility, unlike binding. A person can sign in while
|
|
51054
|
+
* collection is on, turn collection off, then sign out — and a gate here would
|
|
51055
|
+
* leave their verified identity persisted. Re-enable collection later and every
|
|
51056
|
+
* signed-out command projects as that person. Sign-out must drop the binding
|
|
51057
|
+
* whatever the current configuration says, and must do so without creating
|
|
51058
|
+
* observation state for a user who never had any.
|
|
51059
|
+
*/
|
|
51060
|
+
const unbindObservation = (config) => Effect.gen(function* () {
|
|
51061
|
+
const path = yield* Path.Path;
|
|
51062
|
+
yield* clearObservationBinding(path.dirname(config.settingsPath));
|
|
51063
|
+
}).pipe(Effect.ignore);
|
|
51064
|
+
const reportGrant = (grant, waiting) => ({
|
|
51065
|
+
data: {
|
|
51066
|
+
email: grant.email,
|
|
51067
|
+
grantId: grant.grantId,
|
|
51068
|
+
revision: grant.revision,
|
|
51069
|
+
accessExpiresAt: grant.accessExpiresAt
|
|
51070
|
+
},
|
|
51071
|
+
lines: [waiting, `Signed in as ${grant.email}.`]
|
|
51072
|
+
});
|
|
51073
|
+
const login = (config, email) => Effect.gen(function* () {
|
|
51074
|
+
if (email === void 0 || email.trim().length === 0) return yield* Effect.fail(missingEmail);
|
|
51075
|
+
const started = yield* startNativeRequest({
|
|
51076
|
+
origin: config.bootstrapBaseUrl,
|
|
51077
|
+
email,
|
|
51078
|
+
intent: { mode: "login" },
|
|
51079
|
+
waitMs: 6e5
|
|
51080
|
+
}).pipe(Effect.mapError((error) => toFailure(error)));
|
|
51081
|
+
yield* writeStderr(`Open ${started.verificationUri}?requestId=${started.requestId} to continue.\n`).pipe(Effect.catchCause(() => Effect.void));
|
|
51082
|
+
const grant = yield* pollUntilGranted(config.bootstrapBaseUrl, started.deviceCode, started.interval);
|
|
51083
|
+
yield* saveGrant(config, grant);
|
|
51084
|
+
yield* bindObservationToPerson(config, grant);
|
|
51085
|
+
return reportGrant(grant, `Open ${started.verificationUri} to continue.`);
|
|
51086
|
+
});
|
|
51087
|
+
const signup = (config, input) => Effect.gen(function* () {
|
|
51088
|
+
if (input.email === void 0 || input.displayName === void 0 || input.handle === void 0 || input.country === void 0) return yield* Effect.fail(missingEmail);
|
|
51089
|
+
const started = yield* startNativeRequest({
|
|
51090
|
+
origin: config.bootstrapBaseUrl,
|
|
51091
|
+
email: input.email,
|
|
51092
|
+
intent: {
|
|
51093
|
+
mode: "signup",
|
|
51094
|
+
profile: {
|
|
51095
|
+
displayName: input.displayName,
|
|
51096
|
+
handle: input.handle,
|
|
51097
|
+
country: input.country
|
|
51098
|
+
}
|
|
51099
|
+
},
|
|
51100
|
+
waitMs: 6e5
|
|
51101
|
+
}).pipe(Effect.mapError((error) => toFailure(error)));
|
|
51102
|
+
yield* writeStderr(`Open ${started.verificationUri}?requestId=${started.requestId} to continue.\n`).pipe(Effect.catchCause(() => Effect.void));
|
|
51103
|
+
const grant = yield* pollUntilGranted(config.bootstrapBaseUrl, started.deviceCode, started.interval);
|
|
51104
|
+
yield* saveGrant(config, grant);
|
|
51105
|
+
yield* bindObservationToPerson(config, grant);
|
|
51106
|
+
return reportGrant(grant, `Open ${started.verificationUri} to continue.`);
|
|
51107
|
+
});
|
|
51108
|
+
const status$1 = (config, email) => Effect.gen(function* () {
|
|
51109
|
+
if (email === void 0) return yield* Effect.fail(missingEmail);
|
|
51110
|
+
const stored = yield* loadGrant(config, email);
|
|
51111
|
+
if (!usableGrant(stored)) return {
|
|
51112
|
+
data: {
|
|
51113
|
+
authenticated: false,
|
|
51114
|
+
email: email.toLowerCase()
|
|
51115
|
+
},
|
|
51116
|
+
lines: ["Not signed in."]
|
|
51117
|
+
};
|
|
50248
51118
|
return {
|
|
50249
|
-
|
|
50250
|
-
|
|
50251
|
-
|
|
50252
|
-
|
|
50253
|
-
|
|
51119
|
+
data: {
|
|
51120
|
+
authenticated: true,
|
|
51121
|
+
email: stored.email,
|
|
51122
|
+
grantId: stored.grantId,
|
|
51123
|
+
revision: stored.revision,
|
|
51124
|
+
accessExpiresAt: stored.accessExpiresAt
|
|
51125
|
+
},
|
|
51126
|
+
lines: [`Signed in as ${stored.email}.`]
|
|
51127
|
+
};
|
|
51128
|
+
});
|
|
51129
|
+
const logout = (config, email) => Effect.gen(function* () {
|
|
51130
|
+
if (email === void 0) return yield* Effect.fail(missingEmail);
|
|
51131
|
+
const previous = yield* tombstoneGrant(config, email);
|
|
51132
|
+
if (storedGrantSecrets(previous)) yield* unbindObservation(config);
|
|
51133
|
+
let remote = "confirmed";
|
|
51134
|
+
if (previous !== null && storedGrantSecrets(previous)) remote = yield* revokeNativeGrant({
|
|
51135
|
+
origin: config.bootstrapBaseUrl,
|
|
51136
|
+
token: previous.refreshToken,
|
|
51137
|
+
hint: "refresh_token"
|
|
51138
|
+
}).pipe(Effect.as("confirmed"), Effect.catch(() => Effect.succeed("unconfirmed")));
|
|
51139
|
+
return {
|
|
51140
|
+
data: {
|
|
51141
|
+
email: email.toLowerCase(),
|
|
51142
|
+
remote
|
|
51143
|
+
},
|
|
51144
|
+
lines: remote === "confirmed" ? ["Signed out."] : ["Signed out locally. Remote revoke was not confirmed."]
|
|
50254
51145
|
};
|
|
50255
51146
|
});
|
|
50256
51147
|
//#endregion
|
|
@@ -50315,6 +51206,11 @@ function observeInvocation(input, work) {
|
|
|
50315
51206
|
if (input.kind === "completion" || input.kind === "collection_control" || input.host === void 0 || input.projectToken === void 0 || !enabled()) return inactiveWork;
|
|
50316
51207
|
const host = input.host;
|
|
50317
51208
|
const token = Redacted.value(input.projectToken);
|
|
51209
|
+
const binding = input.identifiedPerson;
|
|
51210
|
+
const identifiedPerson = binding === void 0 || input.anonymousIdentity === void 0 ? void 0 : Schema.decodeUnknownOption(ObservationPerson)(binding.authUserId).pipe(Option.map((authUserId) => ({
|
|
51211
|
+
authUserId,
|
|
51212
|
+
eventId: binding.eventId
|
|
51213
|
+
})), Option.getOrUndefined);
|
|
50318
51214
|
let engineering;
|
|
50319
51215
|
try {
|
|
50320
51216
|
Schema.decodeUnknownSync(identifier)(input.invocationId);
|
|
@@ -50346,7 +51242,14 @@ function observeInvocation(input, work) {
|
|
|
50346
51242
|
const started = clock.currentTimeMillisUnsafe();
|
|
50347
51243
|
let dropped = 0;
|
|
50348
51244
|
let pendingCaptures = 0;
|
|
50349
|
-
const
|
|
51245
|
+
const identityProps = input.anonymousIdentity === void 0 ? {} : identifiedPerson === void 0 ? {
|
|
51246
|
+
anonymous_id: input.anonymousIdentity.id,
|
|
51247
|
+
cli_identity_kind: "anonymous"
|
|
51248
|
+
} : {
|
|
51249
|
+
anonymous_id: input.anonymousIdentity.id,
|
|
51250
|
+
cli_identity_kind: "identified"
|
|
51251
|
+
};
|
|
51252
|
+
const engineeringEnvelope = {
|
|
50350
51253
|
capxul_env: input.environment,
|
|
50351
51254
|
producer: "server",
|
|
50352
51255
|
cli_version: input.cliVersion,
|
|
@@ -50361,14 +51264,15 @@ function observeInvocation(input, work) {
|
|
|
50361
51264
|
ci: input.ci ?? false,
|
|
50362
51265
|
tty: input.tty ?? false,
|
|
50363
51266
|
output_mode: input.outputMode ?? "human",
|
|
50364
|
-
...
|
|
50365
|
-
anonymous_id: input.anonymousIdentity.id,
|
|
50366
|
-
cli_identity_kind: "anonymous"
|
|
50367
|
-
},
|
|
51267
|
+
...identityProps,
|
|
50368
51268
|
invocation_id: input.invocationId,
|
|
50369
51269
|
correlation_id: input.invocationId,
|
|
50370
51270
|
journey_id: input.invocationId
|
|
50371
51271
|
};
|
|
51272
|
+
const envelope = {
|
|
51273
|
+
...engineeringEnvelope,
|
|
51274
|
+
...identifiedPerson === void 0 ? {} : { auth_user_id: identifiedPerson.authUserId }
|
|
51275
|
+
};
|
|
50372
51276
|
const capture = (event, properties, identity) => {
|
|
50373
51277
|
if (!enabled()) {
|
|
50374
51278
|
dropped += 1;
|
|
@@ -50397,6 +51301,40 @@ function observeInvocation(input, work) {
|
|
|
50397
51301
|
});
|
|
50398
51302
|
if (textEncoder.encode(body).byteLength > CAPTURE_BYTES || !Queue.offerUnsafe(queue, body)) dropped += 1;
|
|
50399
51303
|
};
|
|
51304
|
+
/**
|
|
51305
|
+
* Join this epoch's anonymous history to the person AUTH verified.
|
|
51306
|
+
*
|
|
51307
|
+
* This is a PostHog lifecycle event, not a catalog event: it carries
|
|
51308
|
+
* the person as `distinct_id` and the epoch as `$anon_distinct_id`,
|
|
51309
|
+
* and deliberately omits `$process_person_profile` so the merge is
|
|
51310
|
+
* processed — every catalog event here sets it false, which is what
|
|
51311
|
+
* keeps those events off the person until this record exists.
|
|
51312
|
+
*
|
|
51313
|
+
* The binding's own stored `eventId` is the record UUID, so PostHog
|
|
51314
|
+
* deduplicates a replayed merge. The same idiom `cli_first_used` uses.
|
|
51315
|
+
*
|
|
51316
|
+
* No `$set` traits: identify traits bypass the per-event redactor, and
|
|
51317
|
+
* this module has no pre-minimized person fact to send. The merge is
|
|
51318
|
+
* the whole payload.
|
|
51319
|
+
*/
|
|
51320
|
+
const captureIdentify = (authUserId, anonymousId, eventId, occurredAt) => {
|
|
51321
|
+
if (!enabled()) {
|
|
51322
|
+
dropped += 1;
|
|
51323
|
+
return;
|
|
51324
|
+
}
|
|
51325
|
+
const body = JSON.stringify({
|
|
51326
|
+
api_key: token,
|
|
51327
|
+
event: "$identify",
|
|
51328
|
+
uuid: eventId,
|
|
51329
|
+
timestamp: new Date(occurredAt).toISOString(),
|
|
51330
|
+
properties: {
|
|
51331
|
+
capxul_env: input.environment,
|
|
51332
|
+
distinct_id: authUserId,
|
|
51333
|
+
$anon_distinct_id: anonymousId
|
|
51334
|
+
}
|
|
51335
|
+
});
|
|
51336
|
+
if (textEncoder.encode(body).byteLength > CAPTURE_BYTES || !Queue.offerUnsafe(queue, body)) dropped += 1;
|
|
51337
|
+
};
|
|
50400
51338
|
const sdkDiagnostics = [];
|
|
50401
51339
|
const client = { capture: (event, properties) => {
|
|
50402
51340
|
if (!enabled()) return;
|
|
@@ -50492,6 +51430,7 @@ function observeInvocation(input, work) {
|
|
|
50492
51430
|
});
|
|
50493
51431
|
capture("cli_first_used", { ...first.props }, firstUse);
|
|
50494
51432
|
}
|
|
51433
|
+
if (identifiedPerson !== void 0 && input.anonymousIdentity !== void 0) captureIdentify(identifiedPerson.authUserId, input.anonymousIdentity.id, identifiedPerson.eventId, clock.currentTimeMillisUnsafe());
|
|
50495
51434
|
if (input.kind !== "parse_refusal") {
|
|
50496
51435
|
const start = Schema.decodeUnknownSync(CliCommandStartedTelemetryEventSchema)({
|
|
50497
51436
|
name: "cli_command_started",
|
|
@@ -50514,7 +51453,7 @@ function observeInvocation(input, work) {
|
|
|
50514
51453
|
const outcome = failure?.outcome ?? "succeeded";
|
|
50515
51454
|
const duration = Math.max(0, clock.currentTimeMillisUnsafe() - started);
|
|
50516
51455
|
const completion = Schema.decodeUnknownSync(completionSchema)({
|
|
50517
|
-
...
|
|
51456
|
+
...engineeringEnvelope,
|
|
50518
51457
|
...input.anonymousIdentity === void 0 ? {} : { posthogDistinctId: input.anonymousIdentity.id },
|
|
50519
51458
|
occurred_at: clock.currentTimeMillisUnsafe(),
|
|
50520
51459
|
schema_version: 1,
|
|
@@ -50531,7 +51470,7 @@ function observeInvocation(input, work) {
|
|
|
50531
51470
|
yield* Effect.annotateCurrentSpan(completion);
|
|
50532
51471
|
yield* logEngineeringInfo("cli.command.completed", completion);
|
|
50533
51472
|
for (const diagnostic of sdkDiagnostics) yield* Effect.logInfo("cli.sdk.operation.failed").pipe(Effect.annotateLogs({
|
|
50534
|
-
...
|
|
51473
|
+
...engineeringEnvelope,
|
|
50535
51474
|
...diagnostic
|
|
50536
51475
|
}));
|
|
50537
51476
|
const metricAttributes = {
|
|
@@ -50689,17 +51628,136 @@ function projectException(properties, command) {
|
|
|
50689
51628
|
}
|
|
50690
51629
|
const boundedLine = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 1e7 ? value : null;
|
|
50691
51630
|
//#endregion
|
|
50692
|
-
//#region src/output.ts
|
|
50693
|
-
const writeStdout = (text) => Effect.flatMap(Stdio.Stdio, (stdio) => Stream.run(Stream.make(text), stdio.stdout()));
|
|
50694
|
-
const writeStderr = (text) => Effect.flatMap(Stdio.Stdio, (stdio) => Stream.run(Stream.make(text), stdio.stderr()));
|
|
50695
|
-
function renderResult(mode, envelope, lines = []) {
|
|
50696
|
-
if (mode === "json") return writeStdout(`${JSON.stringify(envelope)}\n`);
|
|
50697
|
-
return envelope.outcome === "succeeded" ? writeStdout(`${lines.join("\n")}\n`) : writeStderr(`${envelope.error.code}: ${envelope.error.message}\n`);
|
|
50698
|
-
}
|
|
50699
|
-
//#endregion
|
|
50700
51631
|
//#region src/version.ts
|
|
50701
|
-
const cliVersion = "4.20.0-beta.
|
|
50702
|
-
const sdkVersion = "4.20.0-beta.
|
|
51632
|
+
const cliVersion = "4.20.0-beta.5";
|
|
51633
|
+
const sdkVersion = "4.20.0-beta.5";
|
|
51634
|
+
//#endregion
|
|
51635
|
+
//#region src/platform/collection-policy.ts
|
|
51636
|
+
const decode = Schema.decodeUnknownOption(Schema.fromJsonString(Settings), { onExcessProperty: "error" });
|
|
51637
|
+
const missing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
51638
|
+
const resolveProtectedDirectory = (directory, uid) => {
|
|
51639
|
+
const stateDirectory = lstatSync(directory, { throwIfNoEntry: false });
|
|
51640
|
+
if (stateDirectory && (!stateDirectory.isDirectory() || stateDirectory.uid !== uid || (stateDirectory.mode & 511) !== 448)) return null;
|
|
51641
|
+
let ancestor = directory;
|
|
51642
|
+
while (!lstatSync(ancestor, { throwIfNoEntry: false })) ancestor = dirname(ancestor);
|
|
51643
|
+
ancestor = realpathSync(ancestor);
|
|
51644
|
+
const canonicalDirectory = ancestor;
|
|
51645
|
+
while (true) {
|
|
51646
|
+
const info = statSync(ancestor);
|
|
51647
|
+
if (!info.isDirectory() || info.uid !== 0 && info.uid !== uid || (info.mode & 18) !== 0 && (info.mode & 512) === 0) return null;
|
|
51648
|
+
const parent = dirname(ancestor);
|
|
51649
|
+
if (parent === ancestor) break;
|
|
51650
|
+
ancestor = parent;
|
|
51651
|
+
}
|
|
51652
|
+
return stateDirectory ? canonicalDirectory : void 0;
|
|
51653
|
+
};
|
|
51654
|
+
const collectionPolicy = (settingsPath, forcedDisabled) => () => {
|
|
51655
|
+
if (forcedDisabled) return false;
|
|
51656
|
+
let descriptor;
|
|
51657
|
+
try {
|
|
51658
|
+
const uid = process$1.getuid?.();
|
|
51659
|
+
if (uid === void 0) return false;
|
|
51660
|
+
const file = resolve$1(settingsPath);
|
|
51661
|
+
const directory = dirname(file);
|
|
51662
|
+
const canonicalDirectory = resolveProtectedDirectory(directory, uid);
|
|
51663
|
+
if (canonicalDirectory === null) return false;
|
|
51664
|
+
if (canonicalDirectory === void 0) return true;
|
|
51665
|
+
try {
|
|
51666
|
+
descriptor = openSync(join(canonicalDirectory, basename(file)), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
51667
|
+
} catch (error) {
|
|
51668
|
+
return missing(error);
|
|
51669
|
+
}
|
|
51670
|
+
const info = fstatSync(descriptor);
|
|
51671
|
+
if (!info.isFile() || info.uid !== uid || (info.mode & 511) !== 384 || info.nlink !== 1 || info.size > 65536) return false;
|
|
51672
|
+
const bytes = Buffer.alloc(65537);
|
|
51673
|
+
let length = 0;
|
|
51674
|
+
while (length < bytes.length) {
|
|
51675
|
+
const count = readSync(descriptor, bytes, length, bytes.length - length, null);
|
|
51676
|
+
if (count === 0) break;
|
|
51677
|
+
length += count;
|
|
51678
|
+
}
|
|
51679
|
+
if (length > 65536) return false;
|
|
51680
|
+
const settings = decode(bytes.toString("utf8", 0, length));
|
|
51681
|
+
return Option.isSome(settings) && settings.value.collection.enabled;
|
|
51682
|
+
} catch {
|
|
51683
|
+
return false;
|
|
51684
|
+
} finally {
|
|
51685
|
+
if (descriptor !== void 0) try {
|
|
51686
|
+
closeSync(descriptor);
|
|
51687
|
+
} catch {}
|
|
51688
|
+
}
|
|
51689
|
+
};
|
|
51690
|
+
//#endregion
|
|
51691
|
+
//#region src/runtime/config.ts
|
|
51692
|
+
const defaultHost = "https://us.i.posthog.com";
|
|
51693
|
+
const defaultProjectToken = Redacted.make("phc_BZuAVofrkNXzwpJkuDG7Rdfurir45fj2dudQvJZk847e");
|
|
51694
|
+
const optionalText = (name) => Config.option(Config.string(name));
|
|
51695
|
+
function httpOrigin(value, field) {
|
|
51696
|
+
return Effect.try({
|
|
51697
|
+
try: () => {
|
|
51698
|
+
const url = new URL(value);
|
|
51699
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
51700
|
+
if (!local && url.protocol !== "https:" || local && !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error();
|
|
51701
|
+
if (field === "bootstrap" && /\.convex\.(cloud|site)$/.test(url.hostname)) throw new Error();
|
|
51702
|
+
return url.origin;
|
|
51703
|
+
},
|
|
51704
|
+
catch: () => configurationFailure(`The ${field} URL must be a valid approved origin.`)
|
|
51705
|
+
});
|
|
51706
|
+
}
|
|
51707
|
+
const loadObservationConfig = Effect.gen(function* () {
|
|
51708
|
+
const path = yield* Path.Path;
|
|
51709
|
+
const values = yield* Config.all({
|
|
51710
|
+
cliHome: optionalText("CAPXUL_CLI_HOME"),
|
|
51711
|
+
home: optionalText("HOME"),
|
|
51712
|
+
xdg: optionalText("XDG_CONFIG_HOME"),
|
|
51713
|
+
host: optionalText("CAPXUL_POSTHOG_HOST"),
|
|
51714
|
+
token: Config.option(Config.redacted("CAPXUL_POSTHOG_PROJECT_TOKEN")),
|
|
51715
|
+
disabled: Config.boolean("CAPXUL_TELEMETRY_DISABLED").pipe(Config.withDefault(false))
|
|
51716
|
+
}).pipe(Effect.mapError(() => configurationFailure("CLI environment configuration is invalid.")));
|
|
51717
|
+
let directory = Option.getOrUndefined(values.cliHome);
|
|
51718
|
+
if (directory === void 0) {
|
|
51719
|
+
const configHome = Option.getOrUndefined(values.xdg);
|
|
51720
|
+
const home = Option.getOrUndefined(values.home);
|
|
51721
|
+
if (configHome !== void 0) directory = path.join(configHome, "capxul", "cli");
|
|
51722
|
+
else if (home !== void 0) directory = path.join(home, ".config", "capxul", "cli");
|
|
51723
|
+
else return yield* Effect.fail(configurationFailure("Set HOME or CAPXUL_CLI_HOME."));
|
|
51724
|
+
}
|
|
51725
|
+
const settingsPath = path.join(path.resolve(directory), "settings.json");
|
|
51726
|
+
const host = Option.getOrUndefined(values.host) ?? defaultHost;
|
|
51727
|
+
return {
|
|
51728
|
+
settingsPath,
|
|
51729
|
+
environment: "staging",
|
|
51730
|
+
posthogHost: host === void 0 ? void 0 : yield* httpOrigin(host, "PostHog"),
|
|
51731
|
+
projectToken: Option.getOrUndefined(values.token) ?? defaultProjectToken,
|
|
51732
|
+
forcedDisabled: values.disabled,
|
|
51733
|
+
collectionEnabled: collectionPolicy(settingsPath, values.disabled)
|
|
51734
|
+
};
|
|
51735
|
+
});
|
|
51736
|
+
const loadConfig = Effect.gen(function* () {
|
|
51737
|
+
const observation = yield* loadObservationConfig;
|
|
51738
|
+
const values = yield* Config.all({
|
|
51739
|
+
key: Config.option(Config.redacted("CAPXUL_PUBLISHABLE_KEY")),
|
|
51740
|
+
bootstrap: Config.string("CAPXUL_BOOTSTRAP_URL").pipe(Config.withDefault("https://api.staging.capxul.com"))
|
|
51741
|
+
}).pipe(Effect.mapError(() => configurationFailure("CLI environment configuration is invalid.")));
|
|
51742
|
+
const bootstrapBaseUrl = yield* httpOrigin(values.bootstrap, "bootstrap");
|
|
51743
|
+
return {
|
|
51744
|
+
...observation,
|
|
51745
|
+
bootstrapBaseUrl,
|
|
51746
|
+
publishableKey: Option.getOrUndefined(values.key)
|
|
51747
|
+
};
|
|
51748
|
+
});
|
|
51749
|
+
const collectionStatus = (config) => Effect.gen(function* () {
|
|
51750
|
+
const stored = (yield* readSettings(config.settingsPath))?.collection.enabled ?? null;
|
|
51751
|
+
const enabled = config.collectionEnabled();
|
|
51752
|
+
const configured = config.posthogHost !== void 0 && config.projectToken !== void 0;
|
|
51753
|
+
return {
|
|
51754
|
+
stored,
|
|
51755
|
+
enabled,
|
|
51756
|
+
configured,
|
|
51757
|
+
effective: enabled && configured,
|
|
51758
|
+
reason: config.forcedDisabled ? "environment" : !enabled ? "disabled" : !configured ? "not_configured" : "enabled"
|
|
51759
|
+
};
|
|
51760
|
+
});
|
|
50703
51761
|
//#endregion
|
|
50704
51762
|
//#region src/runtime/command-metadata.ts
|
|
50705
51763
|
var CliCommandMetadata = class extends Context.Service()("capxul-cli/CommandMetadata") {};
|
|
@@ -50724,7 +51782,7 @@ var CliRuntime = class extends Context.Service()("capxul-cli/Runtime") {};
|
|
|
50724
51782
|
const acquireInvocationIdentity = (config, eligible) => Effect.gen(function* () {
|
|
50725
51783
|
if (!eligible || config?.posthogHost === void 0 || config.projectToken === void 0 || !config.collectionEnabled()) return void 0;
|
|
50726
51784
|
const path = yield* Path.Path;
|
|
50727
|
-
const acquired = yield*
|
|
51785
|
+
const acquired = yield* snapshotInvocationIdentity(path.dirname(config.settingsPath));
|
|
50728
51786
|
return acquired.status === "ready" ? acquired : void 0;
|
|
50729
51787
|
}).pipe(Effect.catchCause(() => Effect.succeed(void 0)));
|
|
50730
51788
|
const runInvocation = (runtime, metadata, kind, work, network = false) => Effect.gen(function* () {
|
|
@@ -50748,10 +51806,16 @@ const runInvocation = (runtime, metadata, kind, work, network = false) => Effect
|
|
|
50748
51806
|
kind: invocationKind,
|
|
50749
51807
|
network,
|
|
50750
51808
|
invocationId: runtime.invocationId,
|
|
50751
|
-
...identity === void 0 ? {} : {
|
|
50752
|
-
|
|
50753
|
-
|
|
50754
|
-
|
|
51809
|
+
...identity === void 0 ? {} : {
|
|
51810
|
+
anonymousIdentity: {
|
|
51811
|
+
id: identity.epoch.id,
|
|
51812
|
+
...identity.firstUseClaimed ? { firstUse: identity.firstUse } : {}
|
|
51813
|
+
},
|
|
51814
|
+
...identity.epoch.binding === null ? {} : { identifiedPerson: {
|
|
51815
|
+
authUserId: identity.epoch.binding.authUserId,
|
|
51816
|
+
eventId: identity.epoch.binding.eventId
|
|
51817
|
+
} }
|
|
51818
|
+
},
|
|
50755
51819
|
cliVersion,
|
|
50756
51820
|
sdkVersion,
|
|
50757
51821
|
environment: config?.environment ?? "staging",
|
|
@@ -50795,6 +51859,61 @@ const runCommand = (options, work) => Effect.gen(function* () {
|
|
|
50795
51859
|
}), options.network);
|
|
50796
51860
|
});
|
|
50797
51861
|
//#endregion
|
|
51862
|
+
//#region src/commands/auth.ts
|
|
51863
|
+
const emailFlag = Flag.string("email").pipe(Flag.withDescription("Account email"), Flag.optional);
|
|
51864
|
+
const loginMetadata = {
|
|
51865
|
+
name: "auth.login",
|
|
51866
|
+
capability: "authentication",
|
|
51867
|
+
observation: "command"
|
|
51868
|
+
};
|
|
51869
|
+
const signupMetadata = {
|
|
51870
|
+
...loginMetadata,
|
|
51871
|
+
name: "auth.signup"
|
|
51872
|
+
};
|
|
51873
|
+
const statusMetadata$1 = {
|
|
51874
|
+
...loginMetadata,
|
|
51875
|
+
name: "auth.status"
|
|
51876
|
+
};
|
|
51877
|
+
const logoutMetadata = {
|
|
51878
|
+
...loginMetadata,
|
|
51879
|
+
name: "auth.logout"
|
|
51880
|
+
};
|
|
51881
|
+
const loginCommand = Command.make("login", { email: emailFlag }, ({ email }) => runCommand({
|
|
51882
|
+
...loginMetadata,
|
|
51883
|
+
network: true
|
|
51884
|
+
}, (config) => login(config, Option.getOrUndefined(email)))).pipe(Command.annotate(CliCommandMetadata, loginMetadata), Command.withDescription("Sign in through the browser and resume this process"));
|
|
51885
|
+
const signupCommand = Command.make("signup", {
|
|
51886
|
+
email: emailFlag,
|
|
51887
|
+
displayName: Flag.string("display-name").pipe(Flag.optional),
|
|
51888
|
+
handle: Flag.string("handle").pipe(Flag.optional),
|
|
51889
|
+
country: Flag.string("country").pipe(Flag.optional)
|
|
51890
|
+
}, ({ email, displayName, handle, country }) => runCommand({
|
|
51891
|
+
...signupMetadata,
|
|
51892
|
+
network: true
|
|
51893
|
+
}, (config) => signup(config, {
|
|
51894
|
+
email: Option.getOrUndefined(email),
|
|
51895
|
+
displayName: Option.getOrUndefined(displayName),
|
|
51896
|
+
handle: Option.getOrUndefined(handle),
|
|
51897
|
+
country: Option.getOrUndefined(country)
|
|
51898
|
+
}))).pipe(Command.annotate(CliCommandMetadata, signupMetadata), Command.withDescription("Create an account through the browser and resume this process"));
|
|
51899
|
+
const statusCommand = Command.make("status", { email: emailFlag }, ({ email }) => runCommand({
|
|
51900
|
+
...statusMetadata$1,
|
|
51901
|
+
network: false
|
|
51902
|
+
}, (config) => status$1(config, Option.getOrUndefined(email)))).pipe(Command.annotate(CliCommandMetadata, statusMetadata$1), Command.withDescription("Show whether this CLI holds a native grant"));
|
|
51903
|
+
const logoutCommand = Command.make("logout", { email: emailFlag }, ({ email }) => runCommand({
|
|
51904
|
+
...logoutMetadata,
|
|
51905
|
+
network: true
|
|
51906
|
+
}, (config) => logout(config, Option.getOrUndefined(email)))).pipe(Command.annotate(CliCommandMetadata, logoutMetadata), Command.withDescription("Revoke the native grant and forget local credentials"));
|
|
51907
|
+
const auth = Command.make("auth").pipe(Command.annotate(CliCommandMetadata, {
|
|
51908
|
+
...loginMetadata,
|
|
51909
|
+
name: "auth"
|
|
51910
|
+
}), Command.withDescription("Sign in, inspect, or sign out of this CLI"), Command.withSubcommands([
|
|
51911
|
+
loginCommand,
|
|
51912
|
+
signupCommand,
|
|
51913
|
+
statusCommand,
|
|
51914
|
+
logoutCommand
|
|
51915
|
+
]));
|
|
51916
|
+
//#endregion
|
|
50798
51917
|
//#region src/platform/capxul.ts
|
|
50799
51918
|
const resultValue = (result) => result.ok ? Effect.succeed(result.value) : Effect.fail(result.error);
|
|
50800
51919
|
const acquireClient = (input, onFinalize) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
@@ -50966,7 +52085,11 @@ const command = Command.make("capxul").pipe(Command.annotate(CliCommandMetadata,
|
|
|
50966
52085
|
name: "capxul",
|
|
50967
52086
|
capability: "cli",
|
|
50968
52087
|
observation: "command"
|
|
50969
|
-
}), Command.withDescription("Capxul diagnostics and configuration"), Command.withSubcommands([
|
|
52088
|
+
}), Command.withDescription("Capxul diagnostics and configuration"), Command.withSubcommands([
|
|
52089
|
+
auth,
|
|
52090
|
+
doctor,
|
|
52091
|
+
telemetry
|
|
52092
|
+
]), Command.withGlobalFlags([JsonOutput]));
|
|
50970
52093
|
//#endregion
|
|
50971
52094
|
//#region src/runtime/entry.ts
|
|
50972
52095
|
const completionFailure = (error) => {
|