@capxul/cli 4.20.0-beta.4 → 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 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: "settlement"
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 = [...CONFIGURED_MONEY_ASSETS, ...SYNTHETIC_TEST_ASSETS];
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.Literal("anonymous")),
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
- /** Exact HTTPS origin, no credentials/path/query and no retired cloud deployment. */
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
- }, { message: "must be a canonical credential-free HTTPS origin outside retired Convex cloud hosts" }));
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: NativeHttpsOriginSchema,
9412
- verificationOrigin: NativeHttpsOriginSchema
9413
- }).annotate(strict).check(Schema.makeFilter((c) => c.keyEnvironment === (c.environment === "production" ? "live" : "test"), { message: "key environment must match the configured deployment" })).annotate(strict);
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.4";
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
- const balance = fromWei(wire.rawBalance, wire.decimals, wire.currency);
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 on-chain `balanceOf` integer
19794
- * (the D3 RPC read). `available === balance` for a treasury with no envelope
19795
- * partition yet (a fresh Org reads back $0).
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
- const balance = fromWei(wire.rawBalance, wire.decimals, wire.currency);
19941
- const available = fromWei(wire.rawAvailableBalance, wire.decimals, wire.currency);
19942
- return {
19943
- ...brandOrgTreasury({
19944
- orgId: wire.orgId,
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
- /** Zero `Money` in the USDX-backed display currency (a fresh Org's treasury). */
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
- const balance = zeroMoney();
47106
- return {
47107
- id: toAccountId(`account_${stripPrefix(orgId)}`),
47108
- balance,
47109
- available: balance
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
- balance: treasuryResult.value.balance,
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 payable row holding the most money, or
47626
- * `null` when no row is payable.
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, and three
47629
- * of its shapes cannot become a `Holding`. Each is SKIPPED rather than
47630
- * reported, because a row nobody can render is not a failure of the read:
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
- * - no `decimals` — the raw balance cannot be lifted into major units;
47633
- * - no `symbol` — the asset cannot be named;
47634
- * - a symbol that stands for no supported `CurrencyCode` — the balance
47635
- * cannot be stated as `Money` without inventing a currency for it.
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]`. Ties keep the earlier row, so the same
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 || compareDecimal(holding.available.value, primary.available.value) > 0) primary = holding;
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.assetKind === "native" ? "native" : row.tokenAddress.toLowerCase(),
48124
+ id: row.tokenAddress.toLowerCase(),
48125
+ assetKind: row.assetKind,
47657
48126
  symbol: row.symbol,
47658
- available: fromWei(row.rawBalance, row.decimals, currency)
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 auth = makeAuthMethods({
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
- const person = Schema.String.check(Schema.isPattern(/^[^\s]{1,256}$/u));
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: person,
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: person
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 changeObservationIdentity = (directory, input) => Effect.gen(function* () {
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
- /** Call only for eligible collection. This never infers AUTH identity from stored state. */
50077
- const acquireObservationIdentity = (directory) => changeObservationIdentity(directory, { kind: "acquire" });
50078
- //#endregion
50079
- //#region src/platform/collection-policy.ts
50080
- const decode = Schema.decodeUnknownOption(Schema.fromJsonString(Settings), { onExcessProperty: "error" });
50081
- const missing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
50082
- const resolveProtectedDirectory = (directory, uid) => {
50083
- const stateDirectory = lstatSync(directory, { throwIfNoEntry: false });
50084
- if (stateDirectory && (!stateDirectory.isDirectory() || stateDirectory.uid !== uid || (stateDirectory.mode & 511) !== 448)) return null;
50085
- let ancestor = directory;
50086
- while (!lstatSync(ancestor, { throwIfNoEntry: false })) ancestor = dirname(ancestor);
50087
- ancestor = realpathSync(ancestor);
50088
- const canonicalDirectory = ancestor;
50089
- while (true) {
50090
- const info = statSync(ancestor);
50091
- if (!info.isDirectory() || info.uid !== 0 && info.uid !== uid || (info.mode & 18) !== 0 && (info.mode & 512) === 0) return null;
50092
- const parent = dirname(ancestor);
50093
- if (parent === ancestor) break;
50094
- ancestor = parent;
50095
- }
50096
- return stateDirectory ? canonicalDirectory : void 0;
50097
- };
50098
- const collectionPolicy = (settingsPath, forcedDisabled) => () => {
50099
- if (forcedDisabled) return false;
50100
- let descriptor;
50101
- try {
50102
- const uid = process$1.getuid?.();
50103
- if (uid === void 0) return false;
50104
- const file = resolve$1(settingsPath);
50105
- const directory = dirname(file);
50106
- const canonicalDirectory = resolveProtectedDirectory(directory, uid);
50107
- if (canonicalDirectory === null) return false;
50108
- if (canonicalDirectory === void 0) return true;
50109
- try {
50110
- descriptor = openSync(join(canonicalDirectory, basename(file)), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
50111
- } catch (error) {
50112
- return missing(error);
50113
- }
50114
- const info = fstatSync(descriptor);
50115
- if (!info.isFile() || info.uid !== uid || (info.mode & 511) !== 384 || info.nlink !== 1 || info.size > 65536) return false;
50116
- const bytes = Buffer.alloc(65537);
50117
- let length = 0;
50118
- while (length < bytes.length) {
50119
- const count = readSync(descriptor, bytes, length, bytes.length - length, null);
50120
- if (count === 0) break;
50121
- length += count;
50122
- }
50123
- if (length > 65536) return false;
50124
- const settings = decode(bytes.toString("utf8", 0, length));
50125
- return Option.isSome(settings) && settings.value.collection.enabled;
50126
- } catch {
50127
- return false;
50128
- } finally {
50129
- if (descriptor !== void 0) try {
50130
- closeSync(descriptor);
50131
- } catch {}
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/runtime/config.ts
50187
- const defaultHost = "https://us.i.posthog.com";
50188
- const defaultProjectToken = Redacted.make("phc_BZuAVofrkNXzwpJkuDG7Rdfurir45fj2dudQvJZk847e");
50189
- const optionalText = (name) => Config.option(Config.string(name));
50190
- function httpOrigin(value, field) {
50191
- return Effect.try({
50192
- try: () => {
50193
- const url = new URL(value);
50194
- const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
50195
- if (!local && url.protocol !== "https:" || local && !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error();
50196
- if (field === "bootstrap" && /\.convex\.(cloud|site)$/.test(url.hostname)) throw new Error();
50197
- return url.origin;
50198
- },
50199
- catch: () => configurationFailure(`The ${field} URL must be a valid approved origin.`)
50200
- });
50201
- }
50202
- const loadObservationConfig = Effect.gen(function* () {
50203
- const path = yield* Path.Path;
50204
- const values = yield* Config.all({
50205
- cliHome: optionalText("CAPXUL_CLI_HOME"),
50206
- home: optionalText("HOME"),
50207
- xdg: optionalText("XDG_CONFIG_HOME"),
50208
- host: optionalText("CAPXUL_POSTHOG_HOST"),
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
- settingsPath,
50224
- environment: "staging",
50225
- posthogHost: host === void 0 ? void 0 : yield* httpOrigin(host, "PostHog"),
50226
- projectToken: Option.getOrUndefined(values.token) ?? defaultProjectToken,
50227
- forcedDisabled: values.disabled,
50228
- collectionEnabled: collectionPolicy(settingsPath, values.disabled)
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 loadConfig = Effect.gen(function* () {
50232
- const observation = yield* loadObservationConfig;
50233
- const values = yield* Config.all({
50234
- key: Config.option(Config.redacted("CAPXUL_PUBLISHABLE_KEY")),
50235
- bootstrap: Config.string("CAPXUL_BOOTSTRAP_URL").pipe(Config.withDefault("https://api.staging.capxul.com"))
50236
- }).pipe(Effect.mapError(() => configurationFailure("CLI environment configuration is invalid.")));
50237
- const bootstrapBaseUrl = yield* httpOrigin(values.bootstrap, "bootstrap");
50238
- return {
50239
- ...observation,
50240
- bootstrapBaseUrl,
50241
- publishableKey: Option.getOrUndefined(values.key)
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 collectionStatus = (config) => Effect.gen(function* () {
50245
- const stored = (yield* readSettings(config.settingsPath))?.collection.enabled ?? null;
50246
- const enabled = config.collectionEnabled();
50247
- const configured = config.posthogHost !== void 0 && config.projectToken !== void 0;
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
- stored,
50250
- enabled,
50251
- configured,
50252
- effective: enabled && configured,
50253
- reason: config.forcedDisabled ? "environment" : !enabled ? "disabled" : !configured ? "not_configured" : "enabled"
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 envelope = {
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
- ...input.anonymousIdentity === void 0 ? {} : {
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
- ...envelope,
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
- ...envelope,
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.4";
50702
- const sdkVersion = "4.20.0-beta.4";
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* acquireObservationIdentity(path.dirname(config.settingsPath));
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 ? {} : { anonymousIdentity: {
50752
- id: identity.epoch.id,
50753
- ...identity.firstUseClaimed ? { firstUse: identity.firstUse } : {}
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([doctor, telemetry]), Command.withGlobalFlags([JsonOutput]));
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) => {