@sylphx/sdk 0.11.0 → 0.11.2

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/index.mjs CHANGED
@@ -66,8 +66,11 @@ __export(errors_exports, {
66
66
  TimeoutError: () => TimeoutError,
67
67
  ValidationError: () => ValidationError,
68
68
  exponentialBackoff: () => exponentialBackoff,
69
- getErrorCode: () => getErrorCode,
70
- getErrorMessage: () => getErrorMessage,
69
+ getErrorCode: () => getErrorCode2,
70
+ getErrorDetails: () => getErrorDetails2,
71
+ getErrorMessage: () => getErrorMessage2,
72
+ getSafeErrorMessage: () => getSafeErrorMessage,
73
+ isChallengeRequired: () => isChallengeRequired,
71
74
  isRetryableError: () => isRetryableError,
72
75
  isSylphxError: () => isSylphxError,
73
76
  toSylphxError: () => toSylphxError
@@ -95,21 +98,91 @@ function isRetryableError(error) {
95
98
  }
96
99
  return false;
97
100
  }
98
- function getErrorMessage(error) {
101
+ function getErrorMessage2(error, fallback = "An unknown error occurred") {
99
102
  if (error instanceof Error) {
100
103
  return error.message;
101
104
  }
102
105
  if (typeof error === "string") {
103
106
  return error;
104
107
  }
105
- return "An unknown error occurred";
108
+ if (error && typeof error === "object" && "message" in error) {
109
+ const message2 = error.message;
110
+ if (typeof message2 === "string") return message2;
111
+ }
112
+ if (error && typeof error === "object" && "response" in error) {
113
+ const response = error.response;
114
+ if (response?.data?.message) return response.data.message;
115
+ if (response?.data?.error) return response.data.error;
116
+ }
117
+ return fallback;
106
118
  }
107
- function getErrorCode(error) {
119
+ function getErrorCode2(error) {
108
120
  if (error instanceof SylphxError) {
109
121
  return error.code;
110
122
  }
111
123
  return "UNKNOWN";
112
124
  }
125
+ function getErrorStatus(error) {
126
+ if (!error || typeof error !== "object") return void 0;
127
+ if ("status" in error && typeof error.status === "number") {
128
+ return error.status;
129
+ }
130
+ if ("statusCode" in error && typeof error.statusCode === "number") {
131
+ return error.statusCode;
132
+ }
133
+ return void 0;
134
+ }
135
+ function getSafeErrorKey(error) {
136
+ if (error instanceof SylphxError) return error.code;
137
+ if (!error || typeof error !== "object") return void 0;
138
+ if ("code" in error && typeof error.code === "string") {
139
+ return error.code;
140
+ }
141
+ const status = getErrorStatus(error);
142
+ return status == null ? void 0 : String(status);
143
+ }
144
+ function getErrorDetails2(error, fallbackMessage = "An unknown error occurred") {
145
+ const details = {
146
+ message: getErrorMessage2(error, fallbackMessage)
147
+ };
148
+ const code = getSafeErrorKey(error);
149
+ if (code) {
150
+ Object.assign(details, { code });
151
+ }
152
+ const status = getErrorStatus(error);
153
+ if (status != null) {
154
+ Object.assign(details, { status });
155
+ }
156
+ if (error instanceof Error) {
157
+ Object.assign(details, {
158
+ name: error.name,
159
+ stack: error.stack,
160
+ ...error.cause ? { cause: error.cause } : {}
161
+ });
162
+ }
163
+ return details;
164
+ }
165
+ function getSafeErrorMessage(error, fallback = "Something went wrong. Please try again.") {
166
+ const errorCode = getSafeErrorKey(error);
167
+ if (errorCode && SAFE_ERROR_MESSAGES[errorCode]) return SAFE_ERROR_MESSAGES[errorCode];
168
+ if (error && typeof error === "object") {
169
+ const coded = error;
170
+ if (coded.data?.code && SAFE_ERROR_MESSAGES[coded.data.code]) {
171
+ return SAFE_ERROR_MESSAGES[coded.data.code];
172
+ }
173
+ }
174
+ const rawMessage = getErrorMessage2(error, "");
175
+ if (rawMessage && rawMessage.length < 200 && !rawMessage.includes("\n") && !rawMessage.includes("at ") && !INTERNAL_ERROR_PATTERNS.some((pattern) => pattern.test(rawMessage))) {
176
+ return rawMessage;
177
+ }
178
+ return fallback;
179
+ }
180
+ function isChallengeRequired(err) {
181
+ if (!(err instanceof Error)) return false;
182
+ if (err.message.includes("challenge")) return true;
183
+ const errWithData = err;
184
+ return errWithData.data?.code === "FORBIDDEN";
185
+ }
113
186
  function toSylphxError(error) {
114
187
  if (error instanceof SylphxError) {
115
188
  return error;
@@ -140,7 +213,7 @@ function toSylphxError(error) {
140
213
  }
141
214
  return new SylphxError(error.message, { cause: error });
142
215
  }
143
- return new SylphxError(getErrorMessage(error));
216
+ return new SylphxError(getErrorMessage2(error));
144
217
  }
145
218
  function exponentialBackoff(attempt, baseDelay = BASE_RETRY_DELAY_MS, maxDelay = MAX_RETRY_DELAY_MS) {
146
219
  const exponentialDelay = baseDelay * 2 ** attempt;
@@ -148,7 +221,7 @@ function exponentialBackoff(attempt, baseDelay = BASE_RETRY_DELAY_MS, maxDelay =
148
221
  const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
149
222
  return Math.round(cappedDelay + jitter);
150
223
  }
151
- var ERROR_CODE_STATUS, RETRYABLE_CODES, SylphxError, NetworkError, TimeoutError, AuthenticationError, AuthorizationError, ValidationError, RateLimitError, NotFoundError;
224
+ var ERROR_CODE_STATUS, RETRYABLE_CODES, SylphxError, NetworkError, TimeoutError, AuthenticationError, AuthorizationError, ValidationError, RateLimitError, NotFoundError, INTERNAL_ERROR_PATTERNS, SAFE_ERROR_MESSAGES;
152
225
  var init_errors = __esm({
153
226
  "src/errors.ts"() {
154
227
  "use strict";
@@ -376,6 +449,57 @@ var init_errors = __esm({
376
449
  this.resourceId = options?.resourceId;
377
450
  }
378
451
  };
452
+ INTERNAL_ERROR_PATTERNS = [
453
+ /sql/i,
454
+ /database/i,
455
+ /postgres/i,
456
+ /neon/i,
457
+ /drizzle/i,
458
+ /prisma/i,
459
+ /constraint/i,
460
+ /foreign key/i,
461
+ /unique violation/i,
462
+ /null value/i,
463
+ /column/i,
464
+ /table/i,
465
+ /relation/i,
466
+ /trpc/i,
467
+ /internal server/i,
468
+ /unexpected/i,
469
+ /cannot read propert/i,
470
+ /undefined is not/i,
471
+ /null is not/i,
472
+ /cannot find module/i,
473
+ /econnrefused/i,
474
+ /etimedout/i,
475
+ /enotfound/i
476
+ ];
477
+ SAFE_ERROR_MESSAGES = {
478
+ "400": "Invalid request. Please check your input and try again.",
479
+ "401": "Please sign in to continue.",
480
+ "403": "You don't have permission to perform this action.",
481
+ "404": "The requested resource was not found.",
482
+ "409": "This action conflicts with existing data. Please refresh and try again.",
483
+ "429": "Too many requests. Please wait a moment and try again.",
484
+ "500": "Something went wrong on our end. Please try again later.",
485
+ "502": "Service temporarily unavailable. Please try again in a moment.",
486
+ "503": "Service temporarily unavailable. Please try again in a moment.",
487
+ BAD_REQUEST: "Invalid request. Please check your input.",
488
+ UNAUTHORIZED: "Please sign in to continue.",
489
+ FORBIDDEN: "You don't have permission to perform this action.",
490
+ NOT_FOUND: "The requested item was not found.",
491
+ CONFLICT: "This action conflicts with existing data.",
492
+ TOO_MANY_REQUESTS: "Too many requests. Please wait a moment.",
493
+ INTERNAL_SERVER_ERROR: "Something went wrong. Please try again later.",
494
+ TIMEOUT: "Request timed out. Please try again.",
495
+ PRECONDITION_FAILED: "This action cannot be completed right now.",
496
+ QUOTA_EXCEEDED: "You've reached your usage limit. Please upgrade your plan.",
497
+ PAYMENT_REQUIRED: "Payment is required to continue.",
498
+ INVALID_CREDENTIALS: "Invalid email or password.",
499
+ EMAIL_NOT_VERIFIED: "Please verify your email address.",
500
+ ACCOUNT_LOCKED: "Your account has been locked. Please contact support.",
501
+ SESSION_EXPIRED: "Your session has expired. Please sign in again."
502
+ };
379
503
  }
380
504
  });
381
505
 
@@ -4780,8 +4904,112 @@ var init_webapi = __esm({
4780
4904
  }
4781
4905
  });
4782
4906
 
4907
+ // src/compute.ts
4908
+ import {
4909
+ DEFAULT_MACHINE_SIZE,
4910
+ isMachineSize,
4911
+ MACHINE_CONFIGS,
4912
+ MACHINE_MAX_INSTANCES,
4913
+ MACHINE_RESOURCE_REQUIREMENTS,
4914
+ MACHINE_SIZES,
4915
+ parseMachineSize,
4916
+ resolveMachineConfig,
4917
+ resolveMachineMaxInstances,
4918
+ resolveMachineResources,
4919
+ resolveMachineTierResources,
4920
+ toPublicMachineSize
4921
+ } from "@sylphx/contract/compute";
4922
+
4923
+ // src/config/database-pricing.ts
4924
+ var COMPUTE_PRICE_PER_HOUR_MICRODOLLARS = 8e4;
4925
+ var FREE_COMPUTE_HOURS = 3;
4926
+ var STORAGE_PRICE_PER_GB_MONTH_MICRODOLLARS = 25e4;
4927
+ var FREE_STORAGE_GB = 0.25;
4928
+ var TRANSFER_PRICE_PER_GB_MICRODOLLARS = 9e4;
4929
+ var KV_FREE_STORAGE_GB = 0.25;
4930
+ var HOURS_PER_MONTH = 730;
4931
+
4932
+ // src/config/referrals.ts
4933
+ var DEFAULT_POINTS_REWARD = 100;
4934
+ var DISCOUNT_DURATION_MONTHS = 3;
4935
+ var DISCOUNT_PERCENT = 20;
4936
+ var PREMIUM_TRIAL_DAYS = 7;
4937
+ var REFERRAL_CODE_LENGTH = 8;
4938
+ var REFERRAL_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
4939
+ function generateReferralCode() {
4940
+ const bytes = new Uint8Array(REFERRAL_CODE_LENGTH);
4941
+ crypto.getRandomValues(bytes);
4942
+ let code = "";
4943
+ for (let i = 0; i < REFERRAL_CODE_LENGTH; i++) {
4944
+ code += REFERRAL_CODE_CHARS[bytes[i] % REFERRAL_CODE_CHARS.length];
4945
+ }
4946
+ return code;
4947
+ }
4948
+
4949
+ // src/error-extract.ts
4950
+ function getErrorMessage(error, fallback = "Unknown error") {
4951
+ if (error instanceof Error) {
4952
+ return error.message;
4953
+ }
4954
+ if (typeof error === "string") {
4955
+ return error;
4956
+ }
4957
+ if (error && typeof error === "object" && "message" in error) {
4958
+ const message2 = error.message;
4959
+ if (typeof message2 === "string") {
4960
+ return message2;
4961
+ }
4962
+ }
4963
+ if (error && typeof error === "object" && "response" in error) {
4964
+ const response = error.response;
4965
+ if (response?.data?.message) return response.data.message;
4966
+ if (response?.data?.error) return response.data.error;
4967
+ }
4968
+ return fallback;
4969
+ }
4970
+ function getErrorCode(error) {
4971
+ if (!error || typeof error !== "object") {
4972
+ return void 0;
4973
+ }
4974
+ if ("code" in error && typeof error.code === "string") {
4975
+ return error.code;
4976
+ }
4977
+ if ("status" in error && typeof error.status === "number") {
4978
+ return String(error.status);
4979
+ }
4980
+ if ("statusCode" in error && typeof error.statusCode === "number") {
4981
+ return String(error.statusCode);
4982
+ }
4983
+ return void 0;
4984
+ }
4985
+ function getErrorDetails(error, fallbackMessage = "Unknown error") {
4986
+ const details = {
4987
+ message: getErrorMessage(error, fallbackMessage)
4988
+ };
4989
+ if (error instanceof Error) {
4990
+ details.name = error.name;
4991
+ details.stack = error.stack;
4992
+ if (error.cause) {
4993
+ details.cause = error.cause;
4994
+ }
4995
+ }
4996
+ const code = getErrorCode(error);
4997
+ if (code) {
4998
+ details.code = code;
4999
+ }
5000
+ if (error && typeof error === "object") {
5001
+ if ("status" in error && typeof error.status === "number") {
5002
+ details.status = error.status;
5003
+ } else if ("statusCode" in error && typeof error.statusCode === "number") {
5004
+ details.status = error.statusCode;
5005
+ }
5006
+ }
5007
+ return details;
5008
+ }
5009
+
4783
5010
  // src/connection-url.ts
4784
5011
  var SYLPHX_PROTOCOL = "sylphx:";
5012
+ var DEFAULT_DOMAIN = "api.sylphx.com";
4785
5013
  var DEFAULT_VERSION = "v1";
4786
5014
  var CREDENTIAL_REGEX = /^(pk|sk)_(dev|stg|prod|prev)(?:_[a-z0-9]{12})?_[a-f0-9]{32,64}$/;
4787
5015
  var VERSION_REGEX = /^v[0-9]+$/;
@@ -4813,6 +5041,33 @@ function validateSlug(candidate) {
4813
5041
  }
4814
5042
  return candidate;
4815
5043
  }
5044
+ function validateDomain(domain) {
5045
+ if (!domain || domain.includes("/") || domain.includes("@") || domain.includes(" ")) {
5046
+ fail(`domain "${domain}" is not a valid hostname suffix`);
5047
+ }
5048
+ if (domain.includes("://")) {
5049
+ fail(`domain "${domain}" must not contain a scheme`);
5050
+ }
5051
+ return domain.toLowerCase();
5052
+ }
5053
+ function normaliseVersion(version) {
5054
+ if (version === void 0) return DEFAULT_VERSION;
5055
+ if (version === "") return "";
5056
+ if (!VERSION_REGEX.test(version)) {
5057
+ fail(`version "${version}" must match /^v[0-9]+$/`);
5058
+ }
5059
+ return version;
5060
+ }
5061
+ function buildConnectionUrl(input) {
5062
+ const { credential, slug, domain = DEFAULT_DOMAIN, version = DEFAULT_VERSION } = input;
5063
+ parseCredential(credential);
5064
+ const safeSlug = validateSlug(slug);
5065
+ const safeDomain = validateDomain(domain);
5066
+ const safeVersion = normaliseVersion(version);
5067
+ const host = `${safeSlug}.${safeDomain}`;
5068
+ const pathSuffix = safeVersion ? `/${safeVersion}` : "";
5069
+ return `${SYLPHX_PROTOCOL}//${credential}@${host}${pathSuffix}`;
5070
+ }
4816
5071
  function parseConnectionUrl(url) {
4817
5072
  if (typeof url !== "string" || url.length === 0) {
4818
5073
  fail("url must be a non-empty string");
@@ -5187,6 +5442,1044 @@ async function callApi(config, path, options = {}) {
5187
5442
  }
5188
5443
  }
5189
5444
 
5445
+ // src/csv.ts
5446
+ function escapeCsvField(value) {
5447
+ if (value === null || value === void 0) {
5448
+ return "";
5449
+ }
5450
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
5451
+ return `"${value.replace(/"/g, '""')}"`;
5452
+ }
5453
+ return value;
5454
+ }
5455
+
5456
+ // src/formatting.ts
5457
+ function calculatePercentage(count, total, decimals = 2) {
5458
+ if (total === 0) return 100;
5459
+ const multiplier = 10 ** (decimals + 2);
5460
+ return Math.round(count / total * multiplier) / 10 ** decimals;
5461
+ }
5462
+ function formatMicrodollars(microdollars, options) {
5463
+ return new Intl.NumberFormat("en-US", {
5464
+ style: "currency",
5465
+ currency: "USD",
5466
+ ...options
5467
+ }).format(microdollars / 1e6);
5468
+ }
5469
+ function formatCents(cents) {
5470
+ return new Intl.NumberFormat("en-US", {
5471
+ style: "currency",
5472
+ currency: "USD"
5473
+ }).format(cents / 100);
5474
+ }
5475
+ function formatCurrency(amount, optsOrCompact = false) {
5476
+ const opts = typeof optsOrCompact === "boolean" ? { compact: optsOrCompact } : optsOrCompact;
5477
+ const currency = (opts.currency ?? "USD").toUpperCase();
5478
+ const decimals = opts.decimals ?? 2;
5479
+ if (opts.compact && Math.abs(amount) >= 1e3) {
5480
+ return new Intl.NumberFormat("en-US", {
5481
+ style: "currency",
5482
+ currency,
5483
+ notation: "compact",
5484
+ minimumFractionDigits: 1,
5485
+ maximumFractionDigits: 1
5486
+ }).format(amount);
5487
+ }
5488
+ return new Intl.NumberFormat("en-US", {
5489
+ style: "currency",
5490
+ currency,
5491
+ minimumFractionDigits: decimals,
5492
+ maximumFractionDigits: decimals
5493
+ }).format(amount);
5494
+ }
5495
+ function formatPercent(value) {
5496
+ return `${value >= 0 ? "+" : ""}${value.toFixed(1)}%`;
5497
+ }
5498
+ function formatNumber(num, compact = false) {
5499
+ if (compact && num >= 1e3) {
5500
+ return new Intl.NumberFormat("en-US", {
5501
+ notation: "compact",
5502
+ maximumFractionDigits: 1
5503
+ }).format(num);
5504
+ }
5505
+ if (num >= 1e9) return `${(num / 1e9).toFixed(1)}B`;
5506
+ if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
5507
+ if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
5508
+ return num.toLocaleString();
5509
+ }
5510
+ function formatDuration(ms) {
5511
+ if (ms < 1) return "<1ms";
5512
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
5513
+ return `${(ms / 1e3).toFixed(2)}s`;
5514
+ }
5515
+ function formatBytes(bytes, decimals = 1) {
5516
+ if (bytes == null || bytes === 0 || Number.isNaN(bytes)) return "0 B";
5517
+ const k = 1024;
5518
+ const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
5519
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
5520
+ return `${Number.parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
5521
+ }
5522
+ var BILLING_STATUS_VARIANTS = /* @__PURE__ */ new Map([
5523
+ // Active/healthy states
5524
+ ["healthy", "default"],
5525
+ ["active", "default"],
5526
+ // Pending/low states
5527
+ ["low", "secondary"],
5528
+ ["pending", "secondary"],
5529
+ // Success states
5530
+ ["paid", "success"],
5531
+ ["completed", "success"],
5532
+ // Warning states
5533
+ ["grace_period", "warning"],
5534
+ ["overdue", "warning"],
5535
+ // Error states
5536
+ ["critical", "error"],
5537
+ ["blocked", "error"],
5538
+ ["suspended", "error"],
5539
+ ["failed", "error"]
5540
+ ]);
5541
+ function getBillingStatusVariant(status) {
5542
+ return BILLING_STATUS_VARIANTS.get(status) ?? "outline";
5543
+ }
5544
+ var INVOICE_STATUS_VARIANTS = /* @__PURE__ */ new Map([
5545
+ ["draft", "secondary"],
5546
+ ["pending", "default"],
5547
+ ["paid", "success"],
5548
+ ["overdue", "warning"],
5549
+ ["failed", "error"],
5550
+ ["cancelled", "error"]
5551
+ ]);
5552
+ function getInvoiceStatusVariant(status) {
5553
+ return INVOICE_STATUS_VARIANTS.get(status) ?? "outline";
5554
+ }
5555
+ function parseDate(date) {
5556
+ const d = typeof date === "string" ? new Date(date) : date;
5557
+ return Number.isNaN(d.getTime()) ? null : d;
5558
+ }
5559
+ function formatDate(date, options, fallback = "-") {
5560
+ if (!date) return fallback;
5561
+ const d = parseDate(date);
5562
+ if (!d) return fallback;
5563
+ return d.toLocaleDateString("en-US", {
5564
+ month: "short",
5565
+ day: "numeric",
5566
+ year: "numeric",
5567
+ ...options
5568
+ });
5569
+ }
5570
+ function formatDateTime(date, options, fallback = "-") {
5571
+ if (!date) return fallback;
5572
+ const d = parseDate(date);
5573
+ if (!d) return fallback;
5574
+ return d.toLocaleString("en-US", {
5575
+ month: "short",
5576
+ day: "numeric",
5577
+ hour: "2-digit",
5578
+ minute: "2-digit",
5579
+ ...options
5580
+ });
5581
+ }
5582
+ var relativeTimeFormatter = new Intl.RelativeTimeFormat("en", {
5583
+ numeric: "auto"
5584
+ });
5585
+ var TIME_DIVISIONS = [
5586
+ { amount: 60, unit: "second" },
5587
+ { amount: 60, unit: "minute" },
5588
+ { amount: 24, unit: "hour" },
5589
+ { amount: 7, unit: "day" },
5590
+ { amount: 4.34524, unit: "week" },
5591
+ { amount: 12, unit: "month" },
5592
+ { amount: Number.POSITIVE_INFINITY, unit: "year" }
5593
+ ];
5594
+ function formatRelativeTime(date) {
5595
+ if (!date) return "Never";
5596
+ const d = parseDate(date);
5597
+ if (!d) return "Never";
5598
+ let seconds = (d.getTime() - Date.now()) / 1e3;
5599
+ for (const { amount, unit } of TIME_DIVISIONS) {
5600
+ if (Math.abs(seconds) < amount) {
5601
+ return relativeTimeFormatter.format(Math.round(seconds), unit);
5602
+ }
5603
+ seconds /= amount;
5604
+ }
5605
+ return formatDate(d);
5606
+ }
5607
+ function formatRelativeTimeShort(date) {
5608
+ if (!date) return "Never";
5609
+ const d = parseDate(date);
5610
+ if (!d) return "Never";
5611
+ const diffMs = Date.now() - d.getTime();
5612
+ const diffSecs = Math.floor(diffMs / 1e3);
5613
+ const diffMins = Math.floor(diffMs / 6e4);
5614
+ const diffHours = Math.floor(diffMs / 36e5);
5615
+ const diffDays = Math.floor(diffMs / 864e5);
5616
+ if (diffSecs < 10) return "Just now";
5617
+ if (diffSecs < 60) return `${diffSecs}s ago`;
5618
+ if (diffMins < 60) return `${diffMins}m ago`;
5619
+ if (diffHours < 24) return `${diffHours}h ago`;
5620
+ if (diffDays === 1) return "Yesterday";
5621
+ if (diffDays < 7) return `${diffDays}d ago`;
5622
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
5623
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
5624
+ }
5625
+ function formatMonthYear(date, fallback = "-") {
5626
+ if (!date) return fallback;
5627
+ const d = parseDate(date);
5628
+ if (!d) return fallback;
5629
+ return d.toLocaleDateString("en-US", { month: "long", year: "numeric" });
5630
+ }
5631
+ function formatTime(date, fallback = "-") {
5632
+ if (!date) return fallback;
5633
+ const d = parseDate(date);
5634
+ if (!d) return fallback;
5635
+ return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
5636
+ }
5637
+
5638
+ // src/json.ts
5639
+ function safeJsonParse(input, fallback) {
5640
+ try {
5641
+ return JSON.parse(input);
5642
+ } catch {
5643
+ return fallback ?? null;
5644
+ }
5645
+ }
5646
+
5647
+ // src/utils.ts
5648
+ function getBaseUrl(mode = "relative") {
5649
+ if (typeof globalThis.window !== "undefined") {
5650
+ return mode === "origin" ? globalThis.window.location.origin : "";
5651
+ }
5652
+ if (process.env.NEXT_PUBLIC_APP_URL) {
5653
+ return process.env.NEXT_PUBLIC_APP_URL;
5654
+ }
5655
+ const port = process.env.PORT ?? "3000";
5656
+ return `http://localhost:${port}`;
5657
+ }
5658
+ var HTML_ENTITIES = {
5659
+ "&": "&amp;",
5660
+ "<": "&lt;",
5661
+ ">": "&gt;",
5662
+ '"': "&quot;",
5663
+ "'": "&#039;"
5664
+ };
5665
+ function escapeHtml(str) {
5666
+ return str.replace(/[&<>"']/g, (char) => HTML_ENTITIES[char]);
5667
+ }
5668
+ function generateSlug(text, maxLength) {
5669
+ const slug = text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
5670
+ if (maxLength && slug.length > maxLength) {
5671
+ return slug.slice(0, maxLength).replace(/-+$/, "");
5672
+ }
5673
+ return slug;
5674
+ }
5675
+
5676
+ // src/utils/user-agent.ts
5677
+ function parseUserAgent(ua) {
5678
+ if (!ua) {
5679
+ return { browser: null, os: null, deviceType: null };
5680
+ }
5681
+ return {
5682
+ browser: detectBrowser(ua),
5683
+ os: detectOS(ua),
5684
+ deviceType: detectDeviceType(ua)
5685
+ };
5686
+ }
5687
+ function detectBrowser(ua) {
5688
+ if (ua.includes("Edg/")) {
5689
+ const match = ua.match(/Edg\/(\d+)/);
5690
+ return match ? `Edge ${match[1]}` : "Edge";
5691
+ }
5692
+ if (ua.includes("OPR/") || ua.includes("Opera")) {
5693
+ const match = ua.match(/OPR\/(\d+)/) || ua.match(/Opera\/(\d+)/);
5694
+ return match ? `Opera ${match[1]}` : "Opera";
5695
+ }
5696
+ if (ua.includes("Brave")) {
5697
+ return "Brave";
5698
+ }
5699
+ if (ua.includes("Chrome/")) {
5700
+ const match = ua.match(/Chrome\/(\d+)/);
5701
+ return match ? `Chrome ${match[1]}` : "Chrome";
5702
+ }
5703
+ if (ua.includes("Safari/") && !ua.includes("Chrome")) {
5704
+ const match = ua.match(/Version\/(\d+)/);
5705
+ return match ? `Safari ${match[1]}` : "Safari";
5706
+ }
5707
+ if (ua.includes("Firefox/")) {
5708
+ const match = ua.match(/Firefox\/(\d+)/);
5709
+ return match ? `Firefox ${match[1]}` : "Firefox";
5710
+ }
5711
+ if (ua.includes("MSIE") || ua.includes("Trident/")) {
5712
+ const match = ua.match(/(?:MSIE |rv:)(\d+)/);
5713
+ return match ? `IE ${match[1]}` : "Internet Explorer";
5714
+ }
5715
+ return null;
5716
+ }
5717
+ function detectOS(ua) {
5718
+ if (ua.includes("iPhone") || ua.includes("iPad")) {
5719
+ const match = ua.match(/OS (\d+[_\d]*)/);
5720
+ if (match) {
5721
+ const version = match[1].replace(/_/g, ".");
5722
+ return `iOS ${version}`;
5723
+ }
5724
+ return "iOS";
5725
+ }
5726
+ if (ua.includes("Android")) {
5727
+ const match = ua.match(/Android (\d+[.\d]*)/);
5728
+ return match ? `Android ${match[1]}` : "Android";
5729
+ }
5730
+ if (ua.includes("Mac OS X") || ua.includes("macOS")) {
5731
+ const match = ua.match(/Mac OS X (\d+[_\d]*)/) || ua.match(/macOS (\d+[_\d]*)/);
5732
+ if (match) {
5733
+ const version = match[1].replace(/_/g, ".");
5734
+ return `macOS ${version}`;
5735
+ }
5736
+ return "macOS";
5737
+ }
5738
+ if (ua.includes("Windows NT")) {
5739
+ const match = ua.match(/Windows NT ([\d.]+)/);
5740
+ if (match) {
5741
+ const ntVersion = match[1];
5742
+ const windowsVersions = {
5743
+ "10.0": "Windows 10/11",
5744
+ "6.3": "Windows 8.1",
5745
+ "6.2": "Windows 8",
5746
+ "6.1": "Windows 7",
5747
+ "6.0": "Windows Vista",
5748
+ "5.1": "Windows XP"
5749
+ };
5750
+ return windowsVersions[ntVersion] || `Windows NT ${ntVersion}`;
5751
+ }
5752
+ return "Windows";
5753
+ }
5754
+ if (ua.includes("Linux")) {
5755
+ if (ua.includes("Ubuntu")) return "Ubuntu";
5756
+ if (ua.includes("Fedora")) return "Fedora";
5757
+ if (ua.includes("Debian")) return "Debian";
5758
+ return "Linux";
5759
+ }
5760
+ if (ua.includes("CrOS")) {
5761
+ return "Chrome OS";
5762
+ }
5763
+ return null;
5764
+ }
5765
+ function detectDeviceType(ua) {
5766
+ if (ua.includes("iPad") || ua.includes("Tablet") || ua.includes("Android") && !ua.includes("Mobile")) {
5767
+ return "tablet";
5768
+ }
5769
+ if (ua.includes("iPhone") || ua.includes("iPod") || ua.includes("Android") || ua.includes("Mobile") || ua.includes("webOS") || ua.includes("BlackBerry") || ua.includes("IEMobile") || ua.includes("Opera Mini")) {
5770
+ return "mobile";
5771
+ }
5772
+ if (ua.includes("Windows NT") || ua.includes("Mac OS X") || ua.includes("macOS") || ua.includes("Linux") || ua.includes("CrOS")) {
5773
+ return "desktop";
5774
+ }
5775
+ return null;
5776
+ }
5777
+
5778
+ // src/config/auth.ts
5779
+ var MIN_PASSWORD_LENGTH = 8;
5780
+ var MAX_PASSWORD_LENGTH = 128;
5781
+ var PASSWORD_REQUIREMENTS = {
5782
+ minLength: MIN_PASSWORD_LENGTH,
5783
+ maxLength: MAX_PASSWORD_LENGTH,
5784
+ description: `Must be at least ${MIN_PASSWORD_LENGTH} characters`,
5785
+ placeholder: `Min. ${MIN_PASSWORD_LENGTH} characters`
5786
+ };
5787
+
5788
+ // src/config/billing.ts
5789
+ var BYTES_PER_GB = 1024 * 1024 * 1024;
5790
+ var MICRODOLLARS_PER_CENT = 1e4;
5791
+ var INVOICE_DUE_DAYS = 15;
5792
+ var SERVICE_METRICS = {
5793
+ // KV (Key-Value Store)
5794
+ kv: {
5795
+ operations: "operations",
5796
+ // User-friendly: "100K operations"
5797
+ storage: "storage"
5798
+ // GB-month
5799
+ },
5800
+ // Realtime (Pub/Sub Messaging)
5801
+ realtime: {
5802
+ messages: "messages",
5803
+ // User-friendly: "100K messages"
5804
+ connections: "connections"
5805
+ // SSE subscribe connections
5806
+ },
5807
+ // Other services follow the same pattern
5808
+ ai: {
5809
+ tokens: "tokens"
5810
+ },
5811
+ email: {
5812
+ emails: "emails",
5813
+ marketingEmails: "marketing_emails"
5814
+ },
5815
+ notifications: {
5816
+ sends: "sends"
5817
+ },
5818
+ analytics: {
5819
+ events: "events",
5820
+ forwarding: "forwarding"
5821
+ },
5822
+ storage: {
5823
+ capacity: "capacity",
5824
+ uploads: "uploads",
5825
+ egress: "egress"
5826
+ },
5827
+ auth: {
5828
+ mau: "mau"
5829
+ },
5830
+ flags: {
5831
+ evaluations: "evaluations"
5832
+ },
5833
+ consent: {
5834
+ records: "records"
5835
+ },
5836
+ referrals: {
5837
+ conversions: "conversions"
5838
+ },
5839
+ engagement: {
5840
+ operations: "operations"
5841
+ },
5842
+ billing: {
5843
+ subscriptions: "subscriptions",
5844
+ usageRecords: "usage_records"
5845
+ },
5846
+ search: {
5847
+ documents: "documents",
5848
+ searches: "searches"
5849
+ },
5850
+ webhooks: {
5851
+ deliveries: "deliveries"
5852
+ },
5853
+ monitoring: {
5854
+ errors: "errors"
5855
+ },
5856
+ jobs: {
5857
+ invocations: "invocations",
5858
+ cronSchedules: "cron_schedules"
5859
+ },
5860
+ database: {
5861
+ computeSeconds: "compute_seconds",
5862
+ storage: "storage",
5863
+ dataTransferBytes: "data_transfer_bytes"
5864
+ },
5865
+ deploy: {
5866
+ buildMinutes: "build_minutes"
5867
+ }
5868
+ };
5869
+ var COMPUTE_VCPU_ACTIVE_RATE_MICRODOLLARS = 400;
5870
+ var COMPUTE_VCPU_IDLE_RATE_MICRODOLLARS = 50;
5871
+ var COMPUTE_RAM_RATE_MICRODOLLARS = 167;
5872
+ var BUILD_MINUTE_PRICES = {
5873
+ standard: 14e3,
5874
+ // $0.014/min
5875
+ large: 3e4,
5876
+ // $0.030/min
5877
+ xlarge: 126e3
5878
+ // $0.126/min
5879
+ };
5880
+ var BUILD_SIZE_MULTIPLIERS = {
5881
+ standard: 1,
5882
+ large: 2,
5883
+ xlarge: 9
5884
+ };
5885
+ var BUILD_MINUTES_INCLUDED = {
5886
+ free: 100,
5887
+ pro: 500,
5888
+ team: 2e3,
5889
+ enterprise: 1e4
5890
+ };
5891
+ var CI_BUILD_MINUTE_PRICE_MICRODOLLARS = BUILD_MINUTE_PRICES.standard;
5892
+ var CI_FREE_MINUTES_PER_MONTH = BUILD_MINUTES_INCLUDED.team;
5893
+ var CI_SIZE_MULTIPLIERS = {
5894
+ nano: 1,
5895
+ small: 1,
5896
+ standard: 1,
5897
+ large: 2,
5898
+ xlarge: 4,
5899
+ "2xlarge": 8
5900
+ };
5901
+ var CI_MACOS_SIZE_MULTIPLIERS = {
5902
+ standard: 10,
5903
+ large: 20,
5904
+ xlarge: 40
5905
+ };
5906
+ var CI_MACOS_MULTIPLIER = CI_MACOS_SIZE_MULTIPLIERS.standard;
5907
+ var CREDIT_EXPIRY_MONTHS = 12;
5908
+ var MAX_PAYMENT_ATTEMPTS = 3;
5909
+ var BILLING_ALLOWED_ROLES = ["super_admin", "admin", "billing"];
5910
+ function hasBillingAccess(role) {
5911
+ return BILLING_ALLOWED_ROLES.includes(role);
5912
+ }
5913
+
5914
+ // src/config/console-keys.ts
5915
+ var CONSOLE_APP_SLUG = "sylphx-console";
5916
+ function getEnvPrefix() {
5917
+ const envType = process.env.NEXT_PUBLIC_ENV_TYPE;
5918
+ if (envType === "development") return "dev";
5919
+ if (envType === "staging") return "stg";
5920
+ if (envType === "production") return "prod";
5921
+ return process.env.NODE_ENV === "production" ? "prod" : "dev";
5922
+ }
5923
+
5924
+ // src/config/instance-types.ts
5925
+ var VCPU_MINUTE_RATE = 463;
5926
+ var GIB_MINUTE_RATE = 232;
5927
+ var INSTANCE_TYPE_ALIASES = {
5928
+ "starter-1x": "xs",
5929
+ "standard-1x": "sm",
5930
+ "standard-2x": "md",
5931
+ "performance-m": "lg",
5932
+ "performance-l": "xl",
5933
+ "performance-xl": "2xl"
5934
+ };
5935
+ function resolveCanonicalInstanceType(id) {
5936
+ return INSTANCE_TYPE_ALIASES[id] ?? id;
5937
+ }
5938
+ var INSTANCE_TYPES = {
5939
+ // ---- Canonical T-shirt sizes (ADR-034) ----
5940
+ xs: {
5941
+ id: "xs",
5942
+ name: "XS",
5943
+ cpuLimit: "250m",
5944
+ memoryLimit: "512Mi",
5945
+ cpuRequest: "63m",
5946
+ memoryRequest: "256Mi",
5947
+ vcpus: 0.25,
5948
+ memoryMib: 512,
5949
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
5950
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
5951
+ allowedPlans: ["free", "starter", "pro", "team", "enterprise"],
5952
+ maxReplicas: 1,
5953
+ highlights: ["0.25 vCPU, 0.5 GiB RAM", "Available on all plans", "Ideal for dev/staging"]
5954
+ },
5955
+ sm: {
5956
+ id: "sm",
5957
+ name: "SM",
5958
+ cpuLimit: "500m",
5959
+ memoryLimit: "1Gi",
5960
+ cpuRequest: "125m",
5961
+ memoryRequest: "512Mi",
5962
+ vcpus: 0.5,
5963
+ memoryMib: 1024,
5964
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
5965
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
5966
+ allowedPlans: ["starter", "pro", "team", "enterprise"],
5967
+ maxReplicas: 3,
5968
+ highlights: ["0.5 vCPU, 1 GiB RAM", "Good for lightweight services", "Starter plan default"]
5969
+ },
5970
+ md: {
5971
+ id: "md",
5972
+ name: "MD",
5973
+ cpuLimit: "1000m",
5974
+ memoryLimit: "2Gi",
5975
+ cpuRequest: "250m",
5976
+ memoryRequest: "1Gi",
5977
+ vcpus: 1,
5978
+ memoryMib: 2048,
5979
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
5980
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
5981
+ allowedPlans: ["starter", "pro", "team", "enterprise"],
5982
+ maxReplicas: 5,
5983
+ highlights: ["1 vCPU, 2 GiB RAM", "Good for web apps and APIs", "Pro plan default"]
5984
+ },
5985
+ lg: {
5986
+ id: "lg",
5987
+ name: "LG",
5988
+ cpuLimit: "2000m",
5989
+ memoryLimit: "4Gi",
5990
+ cpuRequest: "500m",
5991
+ memoryRequest: "2Gi",
5992
+ vcpus: 2,
5993
+ memoryMib: 4096,
5994
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
5995
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
5996
+ allowedPlans: ["pro", "team", "enterprise"],
5997
+ maxReplicas: 10,
5998
+ highlights: ["2 vCPUs, 4 GiB RAM", "High-throughput APIs and workers", "Team plan default"]
5999
+ },
6000
+ xl: {
6001
+ id: "xl",
6002
+ name: "XL",
6003
+ cpuLimit: "4000m",
6004
+ memoryLimit: "8Gi",
6005
+ cpuRequest: "1000m",
6006
+ memoryRequest: "4Gi",
6007
+ vcpus: 4,
6008
+ memoryMib: 8192,
6009
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6010
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6011
+ allowedPlans: ["team", "enterprise"],
6012
+ maxReplicas: 10,
6013
+ highlights: ["4 vCPUs, 8 GiB RAM", "CI builds and ML inference", "Enterprise plan default"]
6014
+ },
6015
+ "2xl": {
6016
+ id: "2xl",
6017
+ name: "2XL",
6018
+ cpuLimit: "8000m",
6019
+ memoryLimit: "16Gi",
6020
+ cpuRequest: "2000m",
6021
+ memoryRequest: "8Gi",
6022
+ vcpus: 8,
6023
+ memoryMib: 16384,
6024
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6025
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6026
+ allowedPlans: ["team", "enterprise"],
6027
+ maxReplicas: 20,
6028
+ highlights: ["8 vCPUs, 16 GiB RAM", "Heavy compute and large builds", "Team/Enterprise"]
6029
+ },
6030
+ "4xl": {
6031
+ id: "4xl",
6032
+ name: "4XL",
6033
+ cpuLimit: "16000m",
6034
+ memoryLimit: "32Gi",
6035
+ cpuRequest: "4000m",
6036
+ memoryRequest: "16Gi",
6037
+ vcpus: 16,
6038
+ memoryMib: 32768,
6039
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6040
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6041
+ allowedPlans: ["enterprise"],
6042
+ maxReplicas: 20,
6043
+ highlights: ["16 vCPUs, 32 GiB RAM", "Maximum compute power", "Enterprise exclusive"]
6044
+ },
6045
+ // ---- Legacy names (deprecated — kept for backward compat with DB values) ----
6046
+ /** @deprecated Use 'xs' instead */
6047
+ "starter-1x": {
6048
+ id: "starter-1x",
6049
+ name: "Starter 1x",
6050
+ cpuLimit: "1000m",
6051
+ memoryLimit: "2Gi",
6052
+ cpuRequest: "250m",
6053
+ memoryRequest: "1Gi",
6054
+ vcpus: 1,
6055
+ memoryMib: 2048,
6056
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6057
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6058
+ allowedPlans: ["free", "starter", "pro", "team", "enterprise"],
6059
+ maxReplicas: 3,
6060
+ highlights: ["1 vCPU, 2 GiB RAM", "Available on all plans", "Ideal for lightweight services"],
6061
+ deprecated: true
6062
+ },
6063
+ /** @deprecated Use 'sm' instead */
6064
+ "standard-1x": {
6065
+ id: "standard-1x",
6066
+ name: "Standard 1x",
6067
+ cpuLimit: "2000m",
6068
+ memoryLimit: "4Gi",
6069
+ cpuRequest: "500m",
6070
+ memoryRequest: "2Gi",
6071
+ vcpus: 2,
6072
+ memoryMib: 4096,
6073
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6074
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6075
+ allowedPlans: ["starter", "pro", "team", "enterprise"],
6076
+ maxReplicas: 5,
6077
+ highlights: ["2 vCPUs, 4 GiB RAM", "Good for web apps and APIs", "Starter plan default"],
6078
+ deprecated: true
6079
+ },
6080
+ /** @deprecated Use 'md' instead */
6081
+ "standard-2x": {
6082
+ id: "standard-2x",
6083
+ name: "Standard 2x",
6084
+ cpuLimit: "2000m",
6085
+ memoryLimit: "8Gi",
6086
+ cpuRequest: "500m",
6087
+ memoryRequest: "4Gi",
6088
+ vcpus: 2,
6089
+ memoryMib: 8192,
6090
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6091
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6092
+ allowedPlans: ["starter", "pro", "team", "enterprise"],
6093
+ maxReplicas: 5,
6094
+ highlights: [
6095
+ "2 vCPUs, 8 GiB RAM",
6096
+ "Double memory for data-heavy workloads",
6097
+ "Pro plan default"
6098
+ ],
6099
+ deprecated: true
6100
+ },
6101
+ /** @deprecated Use 'lg' instead */
6102
+ "performance-m": {
6103
+ id: "performance-m",
6104
+ name: "Performance M",
6105
+ cpuLimit: "4000m",
6106
+ memoryLimit: "16Gi",
6107
+ cpuRequest: "1000m",
6108
+ memoryRequest: "8Gi",
6109
+ vcpus: 4,
6110
+ memoryMib: 16384,
6111
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6112
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6113
+ allowedPlans: ["pro", "team", "enterprise"],
6114
+ maxReplicas: 10,
6115
+ highlights: ["4 vCPUs, 16 GiB RAM", "High-throughput APIs and workers", "Team plan default"],
6116
+ deprecated: true
6117
+ },
6118
+ /** @deprecated Use 'xl' instead */
6119
+ "performance-l": {
6120
+ id: "performance-l",
6121
+ name: "Performance L",
6122
+ cpuLimit: "8000m",
6123
+ memoryLimit: "32Gi",
6124
+ cpuRequest: "2000m",
6125
+ memoryRequest: "16Gi",
6126
+ vcpus: 8,
6127
+ memoryMib: 32768,
6128
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6129
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6130
+ allowedPlans: ["team", "enterprise"],
6131
+ maxReplicas: 10,
6132
+ highlights: ["8 vCPUs, 32 GiB RAM", "CI builds and ML inference", "Enterprise plan default"],
6133
+ deprecated: true
6134
+ },
6135
+ /** @deprecated Use '2xl' instead */
6136
+ "performance-xl": {
6137
+ id: "performance-xl",
6138
+ name: "Performance XL",
6139
+ cpuLimit: "16000m",
6140
+ memoryLimit: "64Gi",
6141
+ cpuRequest: "4000m",
6142
+ memoryRequest: "32Gi",
6143
+ vcpus: 16,
6144
+ memoryMib: 65536,
6145
+ vcpuMinuteRateMicrodollars: VCPU_MINUTE_RATE,
6146
+ gbMinuteRateMicrodollars: GIB_MINUTE_RATE,
6147
+ allowedPlans: ["enterprise"],
6148
+ maxReplicas: 20,
6149
+ highlights: ["16 vCPUs, 64 GiB RAM", "Heavy compute and large builds", "Enterprise exclusive"],
6150
+ deprecated: true
6151
+ }
6152
+ };
6153
+ var INSTANCE_TYPE_ORDER = ["xs", "sm", "md", "lg", "xl", "2xl", "4xl"];
6154
+ var LEGACY_INSTANCE_TYPE_ORDER = [
6155
+ "starter-1x",
6156
+ "standard-1x",
6157
+ "standard-2x",
6158
+ "performance-m",
6159
+ "performance-l",
6160
+ "performance-xl"
6161
+ ];
6162
+ var DEFAULT_INSTANCE_TYPE = {
6163
+ free: "xs",
6164
+ starter: "sm",
6165
+ pro: "md",
6166
+ team: "lg",
6167
+ enterprise: "xl"
6168
+ };
6169
+ function getDefaultInstanceType(plan) {
6170
+ return DEFAULT_INSTANCE_TYPE[plan];
6171
+ }
6172
+ function getAvailableInstanceTypes(plan) {
6173
+ return INSTANCE_TYPE_ORDER.map((id) => INSTANCE_TYPES[id]).filter(
6174
+ (t) => t.allowedPlans.includes(plan) && !t.deprecated
6175
+ );
6176
+ }
6177
+ function resolveResources(id) {
6178
+ const t = INSTANCE_TYPES[id];
6179
+ return {
6180
+ requests: { cpu: t.cpuRequest, memory: t.memoryRequest },
6181
+ limits: { cpu: t.cpuLimit, memory: t.memoryLimit }
6182
+ };
6183
+ }
6184
+ function resolveMaxReplicas(id) {
6185
+ return INSTANCE_TYPES[id].maxReplicas;
6186
+ }
6187
+ var DEFAULT_MAX_REPLICAS = 10;
6188
+ function isValidInstanceType(id) {
6189
+ return id in INSTANCE_TYPES;
6190
+ }
6191
+ function validateInstanceTypeForPlan(id, plan) {
6192
+ const canonicalId = resolveCanonicalInstanceType(id);
6193
+ if (!isValidInstanceType(canonicalId)) {
6194
+ return { valid: false, error: `Unknown instance type: ${id}` };
6195
+ }
6196
+ const definition = INSTANCE_TYPES[canonicalId];
6197
+ if (!definition.allowedPlans.includes(plan)) {
6198
+ return {
6199
+ valid: false,
6200
+ error: `Instance type "${definition.name}" is not available on the ${plan} plan`
6201
+ };
6202
+ }
6203
+ return { valid: true };
6204
+ }
6205
+
6206
+ // src/config/platform-plans.ts
6207
+ var PLATFORM_PLANS = {
6208
+ free: {
6209
+ id: "free",
6210
+ name: "Free",
6211
+ priceMonthly: 0,
6212
+ priceAnnual: 0,
6213
+ includedCreditsMicrodollars: 5e6,
6214
+ // $5
6215
+ features: {
6216
+ customDomains: false,
6217
+ sso: false,
6218
+ priorityCi: false,
6219
+ macosCi: false,
6220
+ support: "community",
6221
+ sla: null,
6222
+ rbac: false,
6223
+ advancedAnalytics: false,
6224
+ whiteLabel: false
6225
+ },
6226
+ limits: {
6227
+ maxProjects: 1,
6228
+ maxMembers: 1,
6229
+ maxCustomDomains: 0,
6230
+ maxConcurrentRunners: 1,
6231
+ maxMacosRunners: 0,
6232
+ maxDatabases: 1,
6233
+ ciMaxJobDurationSeconds: 30 * 60,
6234
+ // 30 min
6235
+ apiRateLimitPerMin: 60,
6236
+ auditLogDays: 0,
6237
+ maxReplicas: 1,
6238
+ includedBuildMinutes: 100,
6239
+ includedBandwidthGb: 100,
6240
+ logRetentionDays: 1,
6241
+ buildMachineTier: "standard"
6242
+ },
6243
+ highlights: [
6244
+ "1 project",
6245
+ "$5 compute credits/mo",
6246
+ "100 build minutes",
6247
+ "100 GB bandwidth",
6248
+ "Community support"
6249
+ ],
6250
+ cta: "Start free"
6251
+ },
6252
+ /**
6253
+ * @deprecated ADR-034 removed the Starter tier. Retained for backward compatibility
6254
+ * with existing subscribers. Not shown in pricing UI for new sign-ups.
6255
+ */
6256
+ starter: {
6257
+ id: "starter",
6258
+ name: "Starter",
6259
+ deprecated: true,
6260
+ priceMonthly: 1900,
6261
+ // $19
6262
+ priceAnnual: 18240,
6263
+ // $19 x 12 x 0.80 = $182.40
6264
+ includedCreditsMicrodollars: 19e6,
6265
+ // $19
6266
+ features: {
6267
+ customDomains: true,
6268
+ sso: false,
6269
+ priorityCi: false,
6270
+ macosCi: true,
6271
+ support: "email",
6272
+ sla: null,
6273
+ rbac: false,
6274
+ advancedAnalytics: false,
6275
+ whiteLabel: false
6276
+ },
6277
+ limits: {
6278
+ maxProjects: 10,
6279
+ maxMembers: 10,
6280
+ maxCustomDomains: 5,
6281
+ maxConcurrentRunners: 5,
6282
+ maxMacosRunners: 1,
6283
+ maxDatabases: 10,
6284
+ ciMaxJobDurationSeconds: 60 * 60,
6285
+ // 1 hr
6286
+ apiRateLimitPerMin: 300,
6287
+ auditLogDays: 30,
6288
+ maxReplicas: 3,
6289
+ includedBuildMinutes: 250,
6290
+ includedBandwidthGb: 500,
6291
+ logRetentionDays: 7,
6292
+ buildMachineTier: "standard"
6293
+ },
6294
+ highlights: ["$19 credits/mo", "10 projects", "Custom domains", "macOS CI", "Email support"],
6295
+ cta: "Get started"
6296
+ },
6297
+ pro: {
6298
+ id: "pro",
6299
+ name: "Pro",
6300
+ priceMonthly: 2e3,
6301
+ // $20
6302
+ priceAnnual: 19200,
6303
+ // $20 x 12 x 0.80 = $192 ($16/mo)
6304
+ includedCreditsMicrodollars: 2e7,
6305
+ // $20
6306
+ features: {
6307
+ customDomains: true,
6308
+ sso: false,
6309
+ priorityCi: true,
6310
+ macosCi: true,
6311
+ support: "priority",
6312
+ sla: "99.9%",
6313
+ rbac: true,
6314
+ advancedAnalytics: true,
6315
+ whiteLabel: false
6316
+ },
6317
+ limits: {
6318
+ maxProjects: null,
6319
+ // unlimited
6320
+ maxMembers: 25,
6321
+ maxCustomDomains: null,
6322
+ // unlimited
6323
+ maxConcurrentRunners: 20,
6324
+ maxMacosRunners: 5,
6325
+ maxDatabases: null,
6326
+ // unlimited
6327
+ ciMaxJobDurationSeconds: 2 * 60 * 60,
6328
+ // 2 hrs
6329
+ apiRateLimitPerMin: 1e3,
6330
+ auditLogDays: 90,
6331
+ maxReplicas: 10,
6332
+ includedBuildMinutes: 500,
6333
+ includedBandwidthGb: 1e3,
6334
+ logRetentionDays: 14,
6335
+ buildMachineTier: "standard"
6336
+ },
6337
+ highlights: [
6338
+ "Unlimited projects",
6339
+ "$20 credits/mo",
6340
+ "500 build minutes",
6341
+ "1 TB bandwidth",
6342
+ "Priority support",
6343
+ "SLA 99.9%"
6344
+ ],
6345
+ badge: "Most Popular",
6346
+ cta: "Start Pro"
6347
+ },
6348
+ team: {
6349
+ id: "team",
6350
+ name: "Team",
6351
+ priceMonthly: 2e3,
6352
+ // $20/user
6353
+ priceAnnual: 19200,
6354
+ // $192/user/yr ($16/user/mo)
6355
+ includedCreditsMicrodollars: 2e7,
6356
+ // $20/user
6357
+ perSeat: true,
6358
+ features: {
6359
+ customDomains: true,
6360
+ sso: true,
6361
+ priorityCi: true,
6362
+ macosCi: true,
6363
+ support: "dedicated",
6364
+ sla: "99.95%",
6365
+ rbac: true,
6366
+ advancedAnalytics: true,
6367
+ whiteLabel: true
6368
+ },
6369
+ limits: {
6370
+ maxProjects: null,
6371
+ // unlimited
6372
+ maxMembers: null,
6373
+ // unlimited
6374
+ maxCustomDomains: null,
6375
+ // unlimited
6376
+ maxConcurrentRunners: null,
6377
+ // unlimited
6378
+ maxMacosRunners: null,
6379
+ // unlimited
6380
+ maxDatabases: null,
6381
+ // unlimited
6382
+ ciMaxJobDurationSeconds: 6 * 60 * 60,
6383
+ // 6 hrs
6384
+ apiRateLimitPerMin: 5e3,
6385
+ auditLogDays: 365,
6386
+ maxReplicas: 20,
6387
+ includedBuildMinutes: 2e3,
6388
+ includedBandwidthGb: 5e3,
6389
+ logRetentionDays: 30,
6390
+ buildMachineTier: "large"
6391
+ },
6392
+ highlights: [
6393
+ "$20/user/mo",
6394
+ "SSO / SAML",
6395
+ "2,000 large build minutes",
6396
+ "5 TB bandwidth",
6397
+ "Dedicated support",
6398
+ "SLA 99.95%"
6399
+ ],
6400
+ cta: "Get Team"
6401
+ },
6402
+ enterprise: {
6403
+ id: "enterprise",
6404
+ name: "Enterprise",
6405
+ priceMonthly: null,
6406
+ // custom
6407
+ priceAnnual: null,
6408
+ // custom
6409
+ includedCreditsMicrodollars: 0,
6410
+ // negotiated
6411
+ isCustom: true,
6412
+ features: {
6413
+ customDomains: true,
6414
+ sso: true,
6415
+ priorityCi: true,
6416
+ macosCi: true,
6417
+ support: "dedicated",
6418
+ sla: "Custom",
6419
+ rbac: true,
6420
+ advancedAnalytics: true,
6421
+ whiteLabel: true
6422
+ },
6423
+ limits: {
6424
+ maxProjects: null,
6425
+ maxMembers: null,
6426
+ maxCustomDomains: null,
6427
+ maxConcurrentRunners: null,
6428
+ maxMacosRunners: null,
6429
+ maxDatabases: null,
6430
+ ciMaxJobDurationSeconds: 12 * 60 * 60,
6431
+ apiRateLimitPerMin: 0,
6432
+ // 0 = unlimited / negotiated
6433
+ auditLogDays: 730,
6434
+ maxReplicas: null,
6435
+ // custom
6436
+ includedBuildMinutes: 0,
6437
+ // negotiated
6438
+ includedBandwidthGb: 0,
6439
+ // negotiated
6440
+ logRetentionDays: 90,
6441
+ buildMachineTier: "xlarge"
6442
+ },
6443
+ highlights: [
6444
+ "Custom credit volume",
6445
+ "Volume discounts",
6446
+ "Dedicated infrastructure",
6447
+ "Custom SLA",
6448
+ "White-glove onboarding",
6449
+ "Custom contracts"
6450
+ ],
6451
+ cta: "Contact sales"
6452
+ }
6453
+ };
6454
+ var PLATFORM_PLAN_ORDER = ["free", "pro", "team", "enterprise"];
6455
+ var PLATFORM_PLAN_ORDER_ALL = [
6456
+ "free",
6457
+ "starter",
6458
+ "pro",
6459
+ "team",
6460
+ "enterprise"
6461
+ ];
6462
+ function isPlanDeprecated(planId) {
6463
+ return PLATFORM_PLANS[planId].deprecated === true;
6464
+ }
6465
+ function getActivePlans() {
6466
+ return PLATFORM_PLAN_ORDER.map((id) => PLATFORM_PLANS[id]);
6467
+ }
6468
+ function microsToDollars(microdollars) {
6469
+ return `$${(microdollars / 1e6).toFixed(0)}`;
6470
+ }
6471
+ function centsToDollars(cents) {
6472
+ return `$${(cents / 100).toFixed(0)}`;
6473
+ }
6474
+ function getPlanMonthlyPrice(plan, annual = false) {
6475
+ if (plan.isCustom) return "Custom";
6476
+ const cents = annual && plan.priceAnnual != null ? Math.round(plan.priceAnnual / 12) : plan.priceMonthly;
6477
+ if (cents == null) return "Custom";
6478
+ if (cents === 0) return "$0";
6479
+ const base = centsToDollars(cents);
6480
+ return plan.perSeat ? `${base}/user` : base;
6481
+ }
6482
+
5190
6483
  // src/debug.ts
5191
6484
  var DEBUG_STORAGE_KEY = "sylphx_debug";
5192
6485
  function isDebugEnabled() {
@@ -5880,400 +7173,98 @@ init_errors();
5880
7173
 
5881
7174
  // src/auth.ts
5882
7175
  import { authEndpoints } from "@sylphx/contract";
5883
- async function signIn(config, input) {
5884
- const body = input;
5885
- const endpoint = authEndpoints.signIn;
5886
- return callApi(config, endpoint.path, {
5887
- method: endpoint.method,
5888
- body
5889
- });
5890
- }
5891
- async function signUp(config, input) {
5892
- const endpoint = authEndpoints.signUp;
5893
- return callApi(config, endpoint.path, {
5894
- method: endpoint.method,
5895
- body: input
5896
- });
5897
- }
5898
- async function signOut(config) {
5899
- const endpoint = authEndpoints.signOut;
5900
- await callApi(config, endpoint.path, { method: endpoint.method });
5901
- }
5902
- async function refreshToken(config, token) {
5903
- return callApi(config, "/auth/token", {
5904
- method: "POST",
5905
- body: {
5906
- grant_type: "refresh_token",
5907
- refresh_token: token,
5908
- client_secret: config.secretKey
7176
+
7177
+ // src/dpop.ts
7178
+ var dpop = {
7179
+ /**
7180
+ * Generate a fresh ES256 key pair. Private key is non-extractable
7181
+ * (`extractable: false`) so it can be stored but never serialised.
7182
+ */
7183
+ async generateKeyPair() {
7184
+ const { privateKey, publicKey } = await crypto.subtle.generateKey(
7185
+ { name: "ECDSA", namedCurve: "P-256" },
7186
+ false,
7187
+ ["sign", "verify"]
7188
+ );
7189
+ const publicJwk = await crypto.subtle.exportKey("jwk", publicKey);
7190
+ const thumbprint = await thumbprintFromJwk(sanitisePublicJwk(publicJwk));
7191
+ return { privateKey, publicKey, thumbprint };
7192
+ },
7193
+ /**
7194
+ * Sign a DPoP proof JWT. When `accessToken` is provided, the proof
7195
+ * includes `ath = base64url(sha256(accessToken))` so the resource
7196
+ * server can bind the proof to the token being presented.
7197
+ */
7198
+ async generateProof(opts) {
7199
+ const publicJwkRaw = await crypto.subtle.exportKey("jwk", opts.publicKey);
7200
+ const publicJwk = sanitisePublicJwk(publicJwkRaw);
7201
+ const header = { typ: "dpop+jwt", alg: "ES256", jwk: publicJwk };
7202
+ const payload = {
7203
+ jti: randomJti(),
7204
+ htm: opts.method.toUpperCase(),
7205
+ htu: stripQueryAndFragment(opts.uri),
7206
+ iat: Math.floor(Date.now() / 1e3)
7207
+ };
7208
+ if (opts.accessToken) {
7209
+ payload.ath = await sha256Base64Url(new TextEncoder().encode(opts.accessToken));
5909
7210
  }
5910
- });
5911
- }
5912
- async function verifyEmail(config, token) {
5913
- await callApi(config, "/auth/verify-email", {
5914
- method: "POST",
5915
- body: { token }
5916
- });
5917
- }
5918
- async function forgotPassword(config, email, options = {}) {
5919
- await callApi(config, "/auth/forgot-password", {
5920
- method: "POST",
5921
- body: {
5922
- email,
5923
- ...options.redirectUrl ? { redirectUrl: options.redirectUrl } : {}
5924
- }
5925
- });
5926
- }
5927
- async function resendVerificationEmail(config, email) {
5928
- const endpoint = authEndpoints.resendEmailVerification;
5929
- const body = { email };
5930
- await callApi(config, endpoint.path, {
5931
- method: endpoint.method,
5932
- body
5933
- });
5934
- }
5935
- async function resetPassword(config, input) {
5936
- await callApi(config, "/auth/reset-password", {
5937
- method: "POST",
5938
- body: { token: input.token, password: input.password }
5939
- });
5940
- }
5941
- async function getSession(config) {
5942
- if (!config.accessToken) {
5943
- return { user: null };
5944
- }
5945
- const endpoint = authEndpoints.getSession;
5946
- try {
5947
- const user2 = await callApi(config, endpoint.path, {
5948
- method: endpoint.method
5949
- });
5950
- return { user: user2 };
5951
- } catch {
5952
- return { user: null };
5953
- }
5954
- }
5955
- async function verifyTwoFactor(config, userId, code) {
5956
- return callApi(config, "/auth/verify-2fa", {
5957
- method: "POST",
5958
- body: { userId, code }
5959
- });
5960
- }
5961
- async function introspectToken(config, token, tokenTypeHint) {
5962
- const response = await fetch(buildApiUrl(config, "/auth/introspect"), {
5963
- method: "POST",
5964
- headers: {
5965
- "Content-Type": "application/json",
5966
- // RFC 7662 §2: server-to-server call — authenticate with secret key
5967
- "x-app-secret": config.secretKey ?? ""
5968
- },
5969
- body: JSON.stringify({
5970
- token,
5971
- token_type_hint: tokenTypeHint
5972
- })
5973
- });
5974
- if (!response.ok) {
5975
- return { active: false };
5976
- }
5977
- return response.json();
5978
- }
5979
- async function revokeToken(config, token, options) {
5980
- await fetch(buildApiUrl(config, "/auth/revoke"), {
5981
- method: "POST",
5982
- headers: { "Content-Type": "application/json" },
5983
- body: JSON.stringify({
5984
- token: options?.revokeAll ? void 0 : token,
5985
- client_secret: config.secretKey,
5986
- user_id: options?.userId,
5987
- revoke_all: options?.revokeAll
5988
- })
5989
- });
5990
- }
5991
- async function revokeAllTokens(config, userId) {
5992
- await revokeToken(config, "", { revokeAll: true, userId });
5993
- }
5994
- async function extendedSignUp(config, input) {
5995
- return callApi(config, "/auth/register", {
5996
- method: "POST",
5997
- body: input
5998
- });
5999
- }
6000
- async function inviteUser(config, input) {
6001
- return callApi(config, "/auth/invite", {
6002
- method: "POST",
6003
- body: input
6004
- });
6005
- }
6006
- function normalizeOrgScopedTokenResponse(data) {
6007
- const accessToken = data.accessToken ?? data.access_token;
6008
- if (!accessToken) {
6009
- throw new Error("Invalid org-scoped token response: missing access token");
6010
- }
6011
- return {
6012
- token: accessToken,
6013
- accessToken,
6014
- expiresIn: data.expiresIn ?? data.expires_in,
6015
- tokenType: data.tokenType ?? data.token_type,
6016
- user: data.user
6017
- };
6018
- }
6019
- async function getOrgScopedToken(config, orgId) {
6020
- const data = await callApi(config, "/auth/switch-org", {
6021
- method: "POST",
6022
- body: { orgId }
6023
- });
6024
- return normalizeOrgScopedTokenResponse(data);
6025
- }
6026
- async function switchOrg(config, orgId) {
6027
- return getOrgScopedToken(config, orgId);
6028
- }
6029
- var device = {
6030
- /**
6031
- * Start a device authorization grant.
6032
- *
6033
- * Returns a `DeviceGrant` with `verification_uri_complete` (open this
6034
- * in the user's browser) and `device_code` (use for polling).
6035
- *
6036
- * @example
6037
- * ```typescript
6038
- * const grant = await device.init({
6039
- * baseUrl: 'https://your-app.api.sylphx.com/v1',
6040
- * clientId: 'sylphx-cli',
6041
- * scope: ['org:read', 'project:*'],
6042
- * })
6043
- * openBrowser(grant.verification_uri_complete)
6044
- * ```
6045
- */
6046
- async init(opts) {
6047
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/device`, {
6048
- method: "POST",
6049
- headers: buildDeviceHeaders(opts.userAgent),
6050
- body: JSON.stringify({
6051
- client_id: opts.clientId,
6052
- scope: opts.scope ?? []
6053
- })
6054
- });
6055
- if (!res.ok) throw await deviceError(res, "device.init");
6056
- return await res.json();
6057
- },
6058
- /**
6059
- * Poll a pending grant. Returns `status: 'pending' | 'approved' |
6060
- * 'denied' | 'expired'`. On `approved`, the result carries the OAuth
6061
- * pair (access_token + refresh_token).
6062
- *
6063
- * Callers MUST respect the `interval` returned by `init()` — polling
6064
- * faster than that may return 429 slow_down (RFC 8628 §5.5).
6065
- */
6066
- async poll(opts) {
6067
- const url = new URL(`${opts.baseUrl.replace(/\/$/, "")}/auth/device/poll`);
6068
- url.searchParams.set("device_code", opts.deviceCode);
6069
- const res = await fetch(url.toString(), {
6070
- method: "GET",
6071
- headers: buildDeviceHeaders(opts.userAgent)
6072
- });
6073
- if (!res.ok) throw await deviceError(res, "device.poll");
6074
- return await res.json();
6075
- },
6076
- /**
6077
- * Browser leg — the approving user confirms the grant.
6078
- *
6079
- * Requires a valid platform-issued access token (`Authorization:
6080
- * Bearer <accessToken>`) proving the user is logged in on the
6081
- * Console. Typically called by the Console's `/device` verification
6082
- * page server-side, forwarding the user's session JWT.
6083
- */
6084
- async approve(opts) {
6085
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/device/approve`, {
6086
- method: "POST",
6087
- headers: {
6088
- ...buildDeviceHeaders(opts.userAgent),
6089
- Authorization: `Bearer ${opts.accessToken}`
6090
- },
6091
- body: JSON.stringify({ user_code: opts.userCode })
6092
- });
6093
- if (!res.ok) throw await deviceError(res, "device.approve");
6094
- return await res.json();
6095
- },
6096
- /**
6097
- * Browser leg — the user declines the grant.
6098
- *
6099
- * Requires a valid platform-issued access token just like `approve`.
6100
- */
6101
- async deny(opts) {
6102
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/device/deny`, {
6103
- method: "POST",
6104
- headers: {
6105
- ...buildDeviceHeaders(opts.userAgent),
6106
- Authorization: `Bearer ${opts.accessToken}`
6107
- },
6108
- body: JSON.stringify({ user_code: opts.userCode })
6109
- });
6110
- if (!res.ok) throw await deviceError(res, "device.deny");
6111
- return await res.json();
6112
- }
6113
- };
6114
- function buildDeviceHeaders(userAgent) {
6115
- const headers = {
6116
- "Content-Type": "application/json"
6117
- };
6118
- if (userAgent) headers["User-Agent"] = userAgent;
6119
- return headers;
6120
- }
6121
- var sessions = {
6122
- /**
6123
- * List every active platform session for the authenticated user.
6124
- *
6125
- * Ordering: most-recently-active first.
6126
- *
6127
- * @example
6128
- * ```typescript
6129
- * const { sessions } = await auth.sessions.list({
6130
- * baseUrl: 'https://your-app.api.sylphx.com/v1',
6131
- * accessToken: platformJwt,
6132
- * })
6133
- * ```
6134
- */
6135
- async list(opts) {
6136
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions`, {
6137
- method: "GET",
6138
- headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent)
6139
- });
6140
- if (!res.ok) throw await platformSessionError(res, "sessions.list");
6141
- return await res.json();
6142
- },
6143
- /**
6144
- * Revoke a specific platform session by id.
6145
- *
6146
- * `sessionId` accepts either the prefixed TypeID (`sess_*`) or the
6147
- * raw UUID — the BaaS side normalises via `parseIdOrError`.
6148
- *
6149
- * @example
6150
- * ```typescript
6151
- * await auth.sessions.revoke({
6152
- * baseUrl,
6153
- * accessToken,
6154
- * sessionId: 'sess_01hxyz...',
6155
- * })
6156
- * ```
6157
- */
6158
- async revoke(opts) {
6159
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/revoke`, {
6160
- method: "POST",
6161
- headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent),
6162
- body: JSON.stringify({ sessionId: opts.sessionId })
6163
- });
6164
- if (!res.ok) throw await platformSessionError(res, "sessions.revoke");
6165
- return await res.json();
6166
- },
6167
- /**
6168
- * Revoke every platform session except the one presenting the
6169
- * current access token. Used by "sign me out of all other devices".
6170
- *
6171
- * When the caller's JWT has no `sid` claim (pure-Bearer CLI/CI
6172
- * flows), this degenerates to `revokeAll` — every session is
6173
- * wiped — because there's no "current" row to keep.
6174
- *
6175
- * @example
6176
- * ```typescript
6177
- * const { revokedCount } = await auth.sessions.revokeOther({
6178
- * baseUrl,
6179
- * accessToken,
6180
- * })
6181
- * ```
6182
- */
6183
- async revokeOther(opts) {
6184
- const res = await fetch(
6185
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/revoke-other`,
6186
- {
6187
- method: "POST",
6188
- headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent)
6189
- }
6190
- );
6191
- if (!res.ok) throw await platformSessionError(res, "sessions.revokeOther");
6192
- return await res.json();
6193
- },
6194
- /**
6195
- * Revoke every platform session for the user, including the
6196
- * caller's own. Used by "sign me out everywhere" — after a
6197
- * password change, a compromise scare, or GDPR-style erasure.
6198
- *
6199
- * The response includes the count of sessions that were
6200
- * revoked so the caller can surface it in a toast or audit UI.
6201
- *
6202
- * @example
6203
- * ```typescript
6204
- * const { count } = await auth.sessions.revokeAll({
6205
- * baseUrl,
6206
- * accessToken,
6207
- * })
6208
- * ```
6209
- */
6210
- async revokeAll(opts) {
6211
- const res = await fetch(
6212
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/revoke-all`,
6213
- {
6214
- method: "POST",
6215
- headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent)
6216
- }
7211
+ if (opts.nonce) payload.nonce = opts.nonce;
7212
+ const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)));
7213
+ const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)));
7214
+ const signingInput = `${headerB64}.${payloadB64}`;
7215
+ const signingBytes = new TextEncoder().encode(signingInput);
7216
+ const signingBuf = new Uint8Array(signingBytes.byteLength);
7217
+ signingBuf.set(signingBytes);
7218
+ const sigBuf = await crypto.subtle.sign(
7219
+ { name: "ECDSA", hash: "SHA-256" },
7220
+ opts.privateKey,
7221
+ signingBuf.buffer
6217
7222
  );
6218
- if (!res.ok) throw await platformSessionError(res, "sessions.revokeAll");
6219
- return await res.json();
6220
- },
6221
- /**
6222
- * Rename a platform session (device label).
6223
- *
6224
- * `sessionId` accepts either the prefixed TypeID or the raw UUID;
6225
- * `name` is a user-supplied string (≤100 chars) surfaced in the
6226
- * "Active sessions" Console UI.
6227
- *
6228
- * @example
6229
- * ```typescript
6230
- * await auth.sessions.rename({
6231
- * baseUrl,
6232
- * accessToken,
6233
- * sessionId,
6234
- * name: 'MacBook (work)',
6235
- * })
6236
- * ```
6237
- */
6238
- async rename(opts) {
6239
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/rename`, {
6240
- method: "POST",
6241
- headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent),
6242
- body: JSON.stringify({
6243
- sessionId: opts.sessionId,
6244
- name: opts.name
6245
- })
6246
- });
6247
- if (!res.ok) throw await platformSessionError(res, "sessions.rename");
6248
- return await res.json();
7223
+ const sigB64 = base64UrlEncode(new Uint8Array(sigBuf));
7224
+ return `${signingInput}.${sigB64}`;
6249
7225
  }
6250
7226
  };
6251
- function buildPlatformSessionsHeaders(accessToken, userAgent) {
6252
- const headers = {
6253
- "Content-Type": "application/json",
6254
- Authorization: `Bearer ${accessToken}`
6255
- };
6256
- if (userAgent) headers["User-Agent"] = userAgent;
6257
- return headers;
7227
+ function sanitisePublicJwk(jwk) {
7228
+ if (jwk.kty !== "EC" || jwk.crv !== "P-256" || !jwk.x || !jwk.y) {
7229
+ throw new Error("DPoP expects ES256 (EC P-256) JWK");
7230
+ }
7231
+ return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
7232
+ }
7233
+ async function thumbprintFromJwk(jwk) {
7234
+ const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
7235
+ return sha256Base64Url(new TextEncoder().encode(canonical));
7236
+ }
7237
+ async function sha256Base64Url(data) {
7238
+ const copy = new Uint8Array(data.byteLength);
7239
+ copy.set(data);
7240
+ const digest2 = await crypto.subtle.digest("SHA-256", copy.buffer);
7241
+ return base64UrlEncode(new Uint8Array(digest2));
7242
+ }
7243
+ function base64UrlEncode(bytes) {
7244
+ let s = "";
7245
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
7246
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7247
+ }
7248
+ function stripQueryAndFragment(uri) {
7249
+ try {
7250
+ const u = new URL(uri);
7251
+ return `${u.protocol}//${u.host}${u.pathname}`;
7252
+ } catch {
7253
+ const q = uri.indexOf("?");
7254
+ const h = uri.indexOf("#");
7255
+ const cut = [q, h].filter((i) => i >= 0).sort((a, b) => a - b)[0];
7256
+ return cut === void 0 ? uri : uri.slice(0, cut);
7257
+ }
7258
+ }
7259
+ function randomJti() {
7260
+ const bytes = new Uint8Array(16);
7261
+ crypto.getRandomValues(bytes);
7262
+ return base64UrlEncode(bytes);
6258
7263
  }
7264
+
7265
+ // src/platform-auth.ts
7266
+ init_errors();
6259
7267
  var platformAuth = {
6260
- /**
6261
- * Rotate a Platform refresh token. The presented token is consumed
6262
- * single-use; the response carries a fresh access JWT plus the
6263
- * rotated refresh token that supersedes it.
6264
- *
6265
- * On reuse-detection / expiry the server returns 401 — the SDK
6266
- * preserves the upstream message so callers can pattern-match
6267
- * `"reuse"` per RFC 6819 §5.2.2.3 and scrub local credentials.
6268
- *
6269
- * @example
6270
- * ```typescript
6271
- * const tokens = await auth.platformAuth.refresh({
6272
- * baseUrl: 'https://sylphx.com',
6273
- * refreshToken: stored.refreshToken,
6274
- * })
6275
- * ```
6276
- */
6277
7268
  async refresh(opts) {
6278
7269
  const headers = { "Content-Type": "application/json" };
6279
7270
  if (opts.userAgent) headers["User-Agent"] = opts.userAgent;
@@ -6286,19 +7277,6 @@ var platformAuth = {
6286
7277
  if (!res.ok) throw await platformAuthError(res, "platformAuth.refresh");
6287
7278
  return await res.json();
6288
7279
  },
6289
- /**
6290
- * Revoke a Platform refresh token (logout). Server-side revocation
6291
- * failure is the caller's call to surface — local-credential cleanup
6292
- * is the CLI's responsibility (logout must succeed offline).
6293
- *
6294
- * @example
6295
- * ```typescript
6296
- * await auth.platformAuth.logout({
6297
- * baseUrl: 'https://sylphx.com',
6298
- * refreshToken: stored.refreshToken,
6299
- * })
6300
- * ```
6301
- */
6302
7280
  async logout(opts) {
6303
7281
  const headers = { "Content-Type": "application/json" };
6304
7282
  if (opts.userAgent) headers["User-Agent"] = opts.userAgent;
@@ -6312,7 +7290,6 @@ var platformAuth = {
6312
7290
  }
6313
7291
  };
6314
7292
  async function platformAuthError(res, operation) {
6315
- const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
6316
7293
  const body = await res.text().catch(() => "");
6317
7294
  let code = "platform_auth_error";
6318
7295
  let message2 = `${operation} failed: HTTP ${res.status}`;
@@ -6334,327 +7311,137 @@ async function platformAuthError(res, operation) {
6334
7311
  else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
6335
7312
  else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
6336
7313
  else errorCode = "BAD_REQUEST";
6337
- return new SylphxError2(message2, {
6338
- code: errorCode,
6339
- status: res.status,
6340
- data: { operation, code }
6341
- });
6342
- }
6343
- async function platformSessionError(res, operation) {
6344
- const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
6345
- const body = await res.text().catch(() => "");
6346
- let code = "platform_sessions_error";
6347
- let message2 = `${operation} failed: HTTP ${res.status}`;
6348
- try {
6349
- const parsed = JSON.parse(body);
6350
- if (parsed.error) code = parsed.error;
6351
- if (parsed.error_description) message2 = parsed.error_description;
6352
- else if (parsed.message) message2 = parsed.message;
6353
- } catch {
6354
- }
6355
- let errorCode;
6356
- if (res.status === 401) errorCode = "UNAUTHORIZED";
6357
- else if (res.status === 404) errorCode = "NOT_FOUND";
6358
- else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
6359
- else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
6360
- else errorCode = "BAD_REQUEST";
6361
- return new SylphxError2(message2, {
7314
+ return new SylphxError(message2, {
6362
7315
  code: errorCode,
6363
7316
  status: res.status,
6364
7317
  data: { operation, code }
6365
7318
  });
6366
7319
  }
6367
- async function deviceError(res, operation) {
6368
- const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
6369
- const body = await res.text().catch(() => "");
6370
- let code = "device_flow_error";
6371
- let message2 = `${operation} failed: HTTP ${res.status}`;
6372
- try {
6373
- const parsed = JSON.parse(body);
6374
- if (parsed.error) code = parsed.error;
6375
- if (parsed.error_description) message2 = parsed.error_description;
6376
- } catch {
6377
- }
6378
- return new SylphxError2(message2, {
6379
- code: res.status === 429 ? "TOO_MANY_REQUESTS" : "BAD_REQUEST",
6380
- status: res.status,
6381
- data: { operation, code }
6382
- });
6383
- }
6384
- var password = {
6385
- /**
6386
- * Check whether the authenticated platform user has a password set.
6387
- *
6388
- * Returns `{ hasPassword: true }` for users that signed up with
6389
- * email+password (or later called `set`), `{ hasPassword: false }`
6390
- * for OAuth-only users (e.g. signed up via Google/GitHub).
6391
- *
6392
- * @example
6393
- * ```typescript
6394
- * const { hasPassword } = await auth.password.status({
6395
- * baseUrl: 'https://your-app.api.sylphx.com/v1',
6396
- * accessToken: platformJwt,
6397
- * })
6398
- * ```
6399
- */
6400
- async status(opts) {
6401
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-password/status`, {
6402
- method: "GET",
6403
- headers: buildPlatformPasswordHeaders(opts.accessToken, opts.userAgent)
6404
- });
6405
- if (!res.ok) throw await platformPasswordError(res, "password.status");
7320
+
7321
+ // src/platform-impersonation.ts
7322
+ init_errors();
7323
+ var impersonation = {
7324
+ async start(opts) {
7325
+ const res = await fetch(
7326
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/start`,
7327
+ {
7328
+ method: "POST",
7329
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7330
+ body: JSON.stringify({
7331
+ targetUserId: opts.targetUserId,
7332
+ ...opts.ipAddress !== void 0 && { ipAddress: opts.ipAddress },
7333
+ ...opts.userAgent !== void 0 && { userAgent: opts.userAgent }
7334
+ })
7335
+ }
7336
+ );
7337
+ if (!res.ok) throw await impersonationError(res, "impersonation.start");
6406
7338
  return await res.json();
6407
7339
  },
6408
- /**
6409
- * Set an initial password for an OAuth-only user.
6410
- *
6411
- * Fails with 400 if the user already has a password (use `change`
6412
- * instead), if the password is <8 characters, or if HIBP reports
6413
- * the password as breached. BaaS invalidates every other session
6414
- * for the user (keeping the caller's current one) after a
6415
- * successful set.
6416
- *
6417
- * @example
6418
- * ```typescript
6419
- * await auth.password.set({
6420
- * baseUrl,
6421
- * accessToken,
6422
- * password: 'correct-horse-battery-staple',
6423
- * })
6424
- * ```
6425
- */
6426
- async set(opts) {
6427
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-password/set`, {
7340
+ async end(opts) {
7341
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/end`, {
6428
7342
  method: "POST",
6429
- headers: buildPlatformPasswordHeaders(opts.accessToken, opts.userAgent),
6430
- body: JSON.stringify({
6431
- password: opts.password
6432
- })
7343
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7344
+ body: JSON.stringify(opts.sessionId !== void 0 ? { sessionId: opts.sessionId } : {})
6433
7345
  });
6434
- if (!res.ok) throw await platformPasswordError(res, "password.set");
7346
+ if (!res.ok) throw await impersonationError(res, "impersonation.end");
6435
7347
  return await res.json();
6436
7348
  },
6437
- /**
6438
- * Change an existing password.
6439
- *
6440
- * Verifies `currentPassword` server-side; a mismatch returns 401.
6441
- * OAuth-only users (no existing password) get 400 — use `set`
6442
- * instead. New password must be ≥8 characters and must not be in
6443
- * HIBP's breach database. BaaS invalidates every other session
6444
- * for the user (keeping the caller's current one) after a
6445
- * successful change.
6446
- *
6447
- * @example
6448
- * ```typescript
6449
- * await auth.password.change({
6450
- * baseUrl,
6451
- * accessToken,
6452
- * currentPassword: 'old-plaintext',
6453
- * newPassword: 'new-plaintext',
6454
- * })
6455
- * ```
6456
- */
6457
- async change(opts) {
6458
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-password/change`, {
6459
- method: "POST",
6460
- headers: buildPlatformPasswordHeaders(opts.accessToken, opts.userAgent),
6461
- body: JSON.stringify({
6462
- currentPassword: opts.currentPassword,
6463
- newPassword: opts.newPassword
6464
- })
6465
- });
6466
- if (!res.ok) throw await platformPasswordError(res, "password.change");
7349
+ async info(opts) {
7350
+ const res = await fetch(
7351
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/info/${encodeURIComponent(opts.sessionId)}`,
7352
+ {
7353
+ method: "GET",
7354
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7355
+ }
7356
+ );
7357
+ if (!res.ok) throw await impersonationError(res, "impersonation.info");
7358
+ return await res.json();
7359
+ },
7360
+ async active(opts) {
7361
+ const res = await fetch(
7362
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/active`,
7363
+ {
7364
+ method: "GET",
7365
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7366
+ }
7367
+ );
7368
+ if (!res.ok) throw await impersonationError(res, "impersonation.active");
7369
+ return await res.json();
7370
+ },
7371
+ async startChallenge(opts) {
7372
+ const res = await fetch(
7373
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/start-challenge`,
7374
+ {
7375
+ method: "POST",
7376
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7377
+ body: JSON.stringify({
7378
+ targetUserId: opts.targetUserId,
7379
+ reason: opts.reason
7380
+ })
7381
+ }
7382
+ );
7383
+ if (!res.ok) throw await impersonationError(res, "impersonation.startChallenge");
7384
+ return await res.json();
7385
+ },
7386
+ async startStepup(opts) {
7387
+ const res = await fetch(
7388
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/start`,
7389
+ {
7390
+ method: "POST",
7391
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7392
+ body: JSON.stringify({
7393
+ requestId: opts.requestId,
7394
+ challengeKey: opts.challengeKey,
7395
+ assertion: opts.assertion,
7396
+ ...opts.emergencyBypass ? { emergencyBypass: true } : {}
7397
+ })
7398
+ }
7399
+ );
7400
+ if (!res.ok) throw await impersonationError(res, "impersonation.startStepup");
6467
7401
  return await res.json();
6468
- }
6469
- };
6470
- function buildPlatformPasswordHeaders(accessToken, userAgent) {
6471
- const headers = {
6472
- "Content-Type": "application/json",
6473
- Authorization: `Bearer ${accessToken}`
6474
- };
6475
- if (userAgent) headers["User-Agent"] = userAgent;
6476
- return headers;
6477
- }
6478
- async function platformPasswordError(res, operation) {
6479
- const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
6480
- const body = await res.text().catch(() => "");
6481
- let code = "platform_password_error";
6482
- let message2 = `${operation} failed: HTTP ${res.status}`;
6483
- try {
6484
- const parsed = JSON.parse(body);
6485
- if (parsed.error) code = parsed.error;
6486
- if (parsed.error_description) message2 = parsed.error_description;
6487
- else if (parsed.message) message2 = parsed.message;
6488
- } catch {
6489
- }
6490
- let errorCode;
6491
- if (res.status === 401) errorCode = "UNAUTHORIZED";
6492
- else if (res.status === 404) errorCode = "NOT_FOUND";
6493
- else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
6494
- else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
6495
- else errorCode = "BAD_REQUEST";
6496
- return new SylphxError2(message2, {
6497
- code: errorCode,
6498
- status: res.status,
6499
- data: { operation, code }
6500
- });
6501
- }
6502
- var user = {
6503
- /**
6504
- * Export every piece of personal data the platform holds about the
6505
- * authenticated user (GDPR Article 20 — right to data portability).
6506
- *
6507
- * The returned record is deliberately loose — it contains the user
6508
- * row, sessions, OAuth accounts, login history, security alerts,
6509
- * organization memberships, subscriptions, per-project memberships,
6510
- * and storage file metadata. Shape varies with customer provisioning.
6511
- *
6512
- * @example
6513
- * ```typescript
6514
- * const data = await auth.user.exportData({
6515
- * baseUrl: 'https://your-app.api.sylphx.com/v1',
6516
- * accessToken: platformJwt,
6517
- * })
6518
- * downloadAsJson(data, 'my-sylphx-data.json')
6519
- * ```
6520
- */
6521
- async exportData(opts) {
6522
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export`, {
6523
- method: "GET",
6524
- headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent)
6525
- });
6526
- if (!res.ok) throw await platformUserError(res, "user.exportData");
7402
+ },
7403
+ async respondConsent(opts) {
7404
+ const res = await fetch(
7405
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/impersonation-consent/${encodeURIComponent(opts.requestId)}`,
7406
+ {
7407
+ method: "POST",
7408
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7409
+ body: JSON.stringify({ decision: opts.decision })
7410
+ }
7411
+ );
7412
+ if (!res.ok) throw await impersonationError(res, "impersonation.respondConsent");
6527
7413
  return await res.json();
6528
7414
  },
6529
- /**
6530
- * Permanently delete the authenticated user's account (GDPR Article
6531
- * 17 right to erasure). Cascades through every provisioned project
6532
- * DB, cancels Stripe subscriptions, deletes S3 blobs, and anonymises
6533
- * billing transactions. Emits a `user.deleted` event so downstream
6534
- * systems can clean up their own state.
6535
- *
6536
- * Returns `{ success: true, deletedData: [...] }` on success where
6537
- * `deletedData` lists the resource kinds that were erased.
6538
- *
6539
- * @remarks
6540
- * This operation is irreversible. Production callers SHOULD require
6541
- * a challenge step (2FA / password confirm / WebAuthn) before
6542
- * invoking this — the BaaS route does NOT perform challenge
6543
- * verification in Phase 2d. ADR-089 Phase 5.11 lands passkey-primary
6544
- * with WebAuthn-required step-up and will add the check at the
6545
- * BaaS boundary.
6546
- *
6547
- * @example
6548
- * ```typescript
6549
- * const result = await auth.user.deleteAccount({
6550
- * baseUrl,
6551
- * accessToken,
6552
- * reason: 'user_request',
6553
- * })
6554
- * if (result.success) signOutAndRedirect('/goodbye')
6555
- * ```
6556
- */
6557
- async deleteAccount(opts) {
6558
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/account`, {
6559
- method: "DELETE",
6560
- headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent),
6561
- body: JSON.stringify({
6562
- ...opts.reason !== void 0 && { reason: opts.reason }
6563
- })
6564
- });
6565
- if (!res.ok) throw await platformUserError(res, "user.deleteAccount");
7415
+ async listRequests(opts) {
7416
+ const params = new URLSearchParams();
7417
+ if (opts.filter?.operatorId) params.set("operatorId", opts.filter.operatorId);
7418
+ if (opts.filter?.targetUserId) params.set("targetUserId", opts.filter.targetUserId);
7419
+ if (opts.filter?.status) params.set("status", opts.filter.status);
7420
+ if (opts.filter?.limit != null) params.set("limit", String(opts.filter.limit));
7421
+ const qs = params.toString();
7422
+ const res = await fetch(
7423
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/requests${qs ? `?${qs}` : ""}`,
7424
+ {
7425
+ method: "GET",
7426
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7427
+ }
7428
+ );
7429
+ if (!res.ok) throw await impersonationError(res, "impersonation.listRequests");
6566
7430
  return await res.json();
6567
7431
  },
6568
- /**
6569
- * Async GDPR Article 20 export job API (ADR-089 Phase 5.5).
6570
- *
6571
- * `user.exportData` above is the Phase 2d synchronous shortcut —
6572
- * kept for backward compat but production callers SHOULD prefer the
6573
- * async flow: large users routinely exceed a single HTTP deadline
6574
- * during enumeration.
6575
- *
6576
- * Typical flow:
6577
- *
6578
- * ```ts
6579
- * const job = await auth.user.exports.initiate({ baseUrl, accessToken })
6580
- * // Poll until terminal:
6581
- * while (true) {
6582
- * const cur = await auth.user.exports.status({ baseUrl, accessToken, id: job.id })
6583
- * if (cur.status === 'complete') break
6584
- * if (cur.status === 'failed') throw new Error(cur.errorMessage ?? 'export failed')
6585
- * await new Promise(r => setTimeout(r, 2000))
6586
- * }
6587
- * const blob = await auth.user.exports.download({ baseUrl, accessToken, id: job.id })
6588
- * saveAs(blob, 'sylphx-export.json')
6589
- * ```
6590
- *
6591
- * Rate limit: 1 `initiate` per 24h per user. Polling + downloading
6592
- * are NOT rate-limited through that bucket (they're cheap reads).
6593
- */
6594
- exports: {
6595
- /**
6596
- * Kick off an export job. Returns the job row in `pending` status
6597
- * with a 202-Accepted semantic — the HTTP layer has accepted the
6598
- * request but the payload is not yet materialized. Poll
6599
- * `status({ id })` until `status === 'complete'`.
6600
- */
6601
- async initiate(opts) {
6602
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export`, {
7432
+ async endSession(opts) {
7433
+ const res = await fetch(
7434
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/end/${encodeURIComponent(opts.requestId)}`,
7435
+ {
6603
7436
  method: "POST",
6604
- headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent),
6605
- body: JSON.stringify(opts.format !== void 0 ? { format: opts.format } : {})
6606
- });
6607
- if (!res.ok) throw await platformUserError(res, "user.exports.initiate");
6608
- return await res.json();
6609
- },
6610
- /**
6611
- * Read the current state of an in-flight or completed export job.
6612
- * Returns 404 (via thrown `SylphxError`) if the job id is unknown
6613
- * OR owned by a different user — cross-user probes can't
6614
- * distinguish the two.
6615
- */
6616
- async status(opts) {
6617
- const res = await fetch(
6618
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export/${encodeURIComponent(
6619
- opts.id
6620
- )}`,
6621
- {
6622
- method: "GET",
6623
- headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent)
6624
- }
6625
- );
6626
- if (!res.ok) throw await platformUserError(res, "user.exports.status");
6627
- return await res.json();
6628
- },
6629
- /**
6630
- * Download the completed export payload. The BaaS route returns a
6631
- * 302 to a freshly-signed object-storage URL; we follow the redirect
6632
- * (standard `fetch` default) and resolve to the raw `Blob`.
6633
- *
6634
- * The integrity headers `X-Sylphx-Export-Sha256` + `X-Sylphx-Export-Size`
6635
- * are available on the final response — CLI consumers SHOULD verify
6636
- * the SHA-256 client-side before handing the archive to the user.
6637
- */
6638
- async download(opts) {
6639
- const res = await fetch(
6640
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export/${encodeURIComponent(
6641
- opts.id
6642
- )}/download`,
6643
- {
6644
- method: "GET",
6645
- headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent)
6646
- }
6647
- );
6648
- if (!res.ok) throw await platformUserError(res, "user.exports.download");
6649
- const sha256 = res.headers.get("X-Sylphx-Export-Sha256");
6650
- const sizeHeader = res.headers.get("X-Sylphx-Export-Size");
6651
- const sizeBytes = sizeHeader ? Number.parseInt(sizeHeader, 10) : null;
6652
- const blob = await res.blob();
6653
- return { blob, sha256, sizeBytes: Number.isFinite(sizeBytes) ? sizeBytes : null };
6654
- }
7437
+ headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7438
+ }
7439
+ );
7440
+ if (!res.ok) throw await impersonationError(res, "impersonation.endSession");
7441
+ return await res.json();
6655
7442
  }
6656
7443
  };
6657
- function buildPlatformUserHeaders(accessToken, userAgent) {
7444
+ function buildImpersonationHeaders(accessToken, userAgent) {
6658
7445
  const headers = {
6659
7446
  "Content-Type": "application/json",
6660
7447
  Authorization: `Bearer ${accessToken}`
@@ -6662,10 +7449,9 @@ function buildPlatformUserHeaders(accessToken, userAgent) {
6662
7449
  if (userAgent) headers["User-Agent"] = userAgent;
6663
7450
  return headers;
6664
7451
  }
6665
- async function platformUserError(res, operation) {
6666
- const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
7452
+ async function impersonationError(res, operation) {
6667
7453
  const body = await res.text().catch(() => "");
6668
- let code = "platform_user_error";
7454
+ let code = "platform_impersonation_error";
6669
7455
  let message2 = `${operation} failed: HTTP ${res.status}`;
6670
7456
  try {
6671
7457
  const parsed = JSON.parse(body);
@@ -6676,16 +7462,20 @@ async function platformUserError(res, operation) {
6676
7462
  }
6677
7463
  let errorCode;
6678
7464
  if (res.status === 401) errorCode = "UNAUTHORIZED";
7465
+ else if (res.status === 403) errorCode = "UNAUTHORIZED";
6679
7466
  else if (res.status === 404) errorCode = "NOT_FOUND";
7467
+ else if (res.status === 409) errorCode = "CONFLICT";
6680
7468
  else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
6681
7469
  else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
6682
7470
  else errorCode = "BAD_REQUEST";
6683
- return new SylphxError2(message2, {
7471
+ return new SylphxError(message2, {
6684
7472
  code: errorCode,
6685
7473
  status: res.status,
6686
7474
  data: { operation, code }
6687
7475
  });
6688
7476
  }
7477
+
7478
+ // src/platform-jwt.ts
6689
7479
  var jwtJwksCache = null;
6690
7480
  function resetPlatformJwksCache() {
6691
7481
  jwtJwksCache = null;
@@ -6797,24 +7587,167 @@ var cookies = {
6797
7587
  return body;
6798
7588
  }
6799
7589
  };
7590
+
7591
+ // src/platform-oauth.ts
7592
+ init_errors();
7593
+
7594
+ // src/oauth-token.ts
7595
+ init_errors();
7596
+ var OAUTH_TOKEN_ERROR_CODES = /* @__PURE__ */ new Set([
7597
+ "invalid_request",
7598
+ "invalid_client",
7599
+ "invalid_grant",
7600
+ "unauthorized_client",
7601
+ "unsupported_grant_type",
7602
+ "invalid_scope",
7603
+ "authorization_pending",
7604
+ "slow_down",
7605
+ "access_denied",
7606
+ "expired_token"
7607
+ ]);
7608
+ function isRecord(value) {
7609
+ return typeof value === "object" && value !== null;
7610
+ }
7611
+ function requireString(record, key) {
7612
+ const value = record[key];
7613
+ if (typeof value !== "string") throw new Error(`Invalid OAuth token field: ${key}`);
7614
+ return value;
7615
+ }
7616
+ function optionalString(record, key) {
7617
+ const value = record[key];
7618
+ if (value === void 0) return void 0;
7619
+ if (typeof value !== "string") throw new Error(`Invalid OAuth token field: ${key}`);
7620
+ return value;
7621
+ }
7622
+ function optionalField(key, value) {
7623
+ return value === void 0 ? {} : { [key]: value };
7624
+ }
7625
+ function requireNumber(record, key) {
7626
+ const value = record[key];
7627
+ if (typeof value !== "number") throw new Error(`Invalid OAuth token field: ${key}`);
7628
+ return value;
7629
+ }
7630
+ function assertGrantType(record, grantType) {
7631
+ if (record.grant_type !== grantType) {
7632
+ throw new Error(`Invalid OAuth grant_type: expected ${grantType}`);
7633
+ }
7634
+ }
7635
+ function decodeOAuthTokenRequest(input) {
7636
+ if (!isRecord(input)) throw new Error("Invalid OAuth token request");
7637
+ switch (input.grant_type) {
7638
+ case "authorization_code":
7639
+ assertGrantType(input, "authorization_code");
7640
+ return {
7641
+ grant_type: "authorization_code",
7642
+ code: requireString(input, "code"),
7643
+ redirect_uri: requireString(input, "redirect_uri"),
7644
+ client_id: requireString(input, "client_id"),
7645
+ code_verifier: requireString(input, "code_verifier"),
7646
+ ...optionalField("client_secret", optionalString(input, "client_secret"))
7647
+ };
7648
+ case "refresh_token":
7649
+ assertGrantType(input, "refresh_token");
7650
+ return {
7651
+ grant_type: "refresh_token",
7652
+ refresh_token: requireString(input, "refresh_token"),
7653
+ client_id: requireString(input, "client_id"),
7654
+ ...optionalField("client_secret", optionalString(input, "client_secret")),
7655
+ ...optionalField("scope", optionalString(input, "scope"))
7656
+ };
7657
+ case "urn:ietf:params:oauth:grant-type:device_code":
7658
+ assertGrantType(input, "urn:ietf:params:oauth:grant-type:device_code");
7659
+ return {
7660
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
7661
+ device_code: requireString(input, "device_code"),
7662
+ client_id: requireString(input, "client_id"),
7663
+ ...optionalField("client_secret", optionalString(input, "client_secret"))
7664
+ };
7665
+ case "client_credentials":
7666
+ assertGrantType(input, "client_credentials");
7667
+ return {
7668
+ grant_type: "client_credentials",
7669
+ client_id: requireString(input, "client_id"),
7670
+ client_secret: requireString(input, "client_secret"),
7671
+ ...optionalField("scope", optionalString(input, "scope"))
7672
+ };
7673
+ default:
7674
+ throw new Error("Unsupported OAuth grant_type");
7675
+ }
7676
+ }
7677
+ function oauthTokenFormBody(input) {
7678
+ const request = decodeOAuthTokenRequest(input);
7679
+ return new URLSearchParams(
7680
+ Object.entries(request).filter(
7681
+ (entry) => typeof entry[1] === "string"
7682
+ )
7683
+ ).toString();
7684
+ }
7685
+ function decodeOAuthTokenResult(value) {
7686
+ if (!isRecord(value)) throw new Error("Invalid OAuth token response");
7687
+ const tokenType = requireString(value, "token_type");
7688
+ if (tokenType !== "Bearer") throw new Error("Invalid OAuth token_type");
7689
+ return {
7690
+ access_token: requireString(value, "access_token"),
7691
+ token_type: "Bearer",
7692
+ expires_in: requireNumber(value, "expires_in"),
7693
+ refresh_token: requireString(value, "refresh_token"),
7694
+ refresh_expires_at: requireString(value, "refresh_expires_at"),
7695
+ scope: requireString(value, "scope")
7696
+ };
7697
+ }
7698
+ function decodeOAuthClientCredentialsResult(value) {
7699
+ if (!isRecord(value)) throw new Error("Invalid OAuth client credentials response");
7700
+ const tokenType = requireString(value, "token_type");
7701
+ if (tokenType !== "Bearer") throw new Error("Invalid OAuth token_type");
7702
+ return {
7703
+ access_token: requireString(value, "access_token"),
7704
+ token_type: "Bearer",
7705
+ expires_in: requireNumber(value, "expires_in"),
7706
+ scope: requireString(value, "scope")
7707
+ };
7708
+ }
7709
+ function decodeOAuthTokenError(value) {
7710
+ if (!isRecord(value)) throw new Error("Invalid OAuth token error response");
7711
+ const error = requireString(value, "error");
7712
+ if (!OAUTH_TOKEN_ERROR_CODES.has(error)) {
7713
+ throw new Error("Invalid OAuth token error code");
7714
+ }
7715
+ return {
7716
+ error,
7717
+ ...optionalField("error_description", optionalString(value, "error_description")),
7718
+ ...optionalField("error_uri", optionalString(value, "error_uri"))
7719
+ };
7720
+ }
7721
+ async function oauthTokenError(res, operation) {
7722
+ const body = await res.text().catch(() => "");
7723
+ let code = "oauth_error";
7724
+ let message2 = `${operation} failed: HTTP ${res.status}`;
7725
+ try {
7726
+ const parsed = decodeOAuthTokenError(JSON.parse(body));
7727
+ code = parsed.error;
7728
+ if (parsed.error_description) message2 = parsed.error_description;
7729
+ } catch {
7730
+ }
7731
+ let errorCode;
7732
+ if (res.status === 401) errorCode = "UNAUTHORIZED";
7733
+ else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
7734
+ else errorCode = "BAD_REQUEST";
7735
+ return new SylphxError(message2, {
7736
+ code: errorCode,
7737
+ status: res.status,
7738
+ data: { oauthError: code }
7739
+ });
7740
+ }
7741
+
7742
+ // src/platform-oauth.ts
6800
7743
  var oauth = {
6801
7744
  /**
6802
7745
  * Mint a platform-audience access token from supplied claims.
6803
7746
  *
6804
7747
  * Service-to-service call — authenticated via
6805
- * `SYLPHX_INTERNAL_TOKEN` shared secret. Phase 6 will migrate this
6806
- * to SPIFFE SVID mTLS (ADR-068).
6807
- *
6808
- * TODO: Phase 6 — prefer SPIFFE SVID over shared-secret auth.
6809
- *
6810
- * @example
6811
- * ```typescript
6812
- * const { accessToken, expiresIn } = await auth.oauth.mintAccessToken({
6813
- * baseUrl: 'https://your-app.api.sylphx.com/v1',
6814
- * internalToken: process.env.SYLPHX_INTERNAL_TOKEN!,
6815
- * claims: { sub: user.id, email: user.email, app_id: 'platform', role: 'member', email_verified: true },
6816
- * })
6817
- * ```
7748
+ * `SYLPHX_INTERNAL_TOKEN` shared secret until ADR-068's
7749
+ * SPIFFE SVID mTLS platform-auth flip makes workload identity the
7750
+ * only accepted internal caller credential.
6818
7751
  */
6819
7752
  async mintAccessToken(opts) {
6820
7753
  const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-jwt/mint`, {
@@ -6829,181 +7762,78 @@ var oauth = {
6829
7762
  if (!res.ok) throw await platformJwtError(res, "oauth.mintAccessToken");
6830
7763
  return await res.json();
6831
7764
  },
6832
- /**
6833
- * Exchange an OAuth 2.0 authorization_code for an access + refresh token
6834
- * pair (ADR-089 Phase 5.1b — RFC 6749 §4.1.3). PKCE S256 mandatory per
6835
- * OAuth 2.1 baseline.
6836
- *
6837
- * @example
6838
- * ```typescript
6839
- * const { verifier, challenge } = await generatePkce()
6840
- * // user redirected to /oauth/authorize?...&code_challenge=<challenge>
6841
- * // ...user approves, browser hits your redirect_uri with ?code=<code>
6842
- * const tokens = await auth.oauth.exchangeAuthorizationCode({
6843
- * baseUrl: 'https://api.sylphx.com/v1',
6844
- * clientId: 'sylphx-console',
6845
- * clientSecret: process.env.CONSOLE_CLIENT_SECRET,
6846
- * code,
6847
- * redirectUri: 'https://console.sylphx.com/auth/callback',
6848
- * codeVerifier: verifier,
6849
- * })
6850
- * ```
6851
- */
6852
7765
  async exchangeAuthorizationCode(opts) {
6853
- const body = {
7766
+ const body = oauthTokenFormBody({
6854
7767
  grant_type: "authorization_code",
6855
7768
  code: opts.code,
6856
7769
  redirect_uri: opts.redirectUri,
6857
7770
  client_id: opts.clientId,
6858
- code_verifier: opts.codeVerifier
6859
- };
6860
- if (opts.clientSecret) body.client_secret = opts.clientSecret;
7771
+ code_verifier: opts.codeVerifier,
7772
+ ...opts.clientSecret ? { client_secret: opts.clientSecret } : {}
7773
+ });
6861
7774
  const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/oauth/token`, {
6862
7775
  method: "POST",
6863
7776
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
6864
- body: new URLSearchParams(body).toString()
7777
+ body
6865
7778
  });
6866
7779
  if (!res.ok) throw await oauthTokenError(res, "oauth.exchangeAuthorizationCode");
6867
- return await res.json();
7780
+ return decodeOAuthTokenResult(await res.json());
6868
7781
  },
6869
- /**
6870
- * Refresh a platform access token using a refresh_token
6871
- * (ADR-089 Phase 5.1b — RFC 6749 §6). Rotation is mandatory — the
6872
- * presented refresh token is consumed and a new one returned.
6873
- *
6874
- * @example
6875
- * ```typescript
6876
- * const tokens = await auth.oauth.refreshAccessToken({
6877
- * baseUrl: 'https://api.sylphx.com/v1',
6878
- * clientId: 'sylphx-console',
6879
- * clientSecret: process.env.CONSOLE_CLIENT_SECRET,
6880
- * refreshToken: stored.refresh_token,
6881
- * })
6882
- * ```
6883
- */
6884
7782
  async refreshAccessToken(opts) {
6885
- const body = {
7783
+ const body = oauthTokenFormBody({
6886
7784
  grant_type: "refresh_token",
6887
7785
  refresh_token: opts.refreshToken,
6888
- client_id: opts.clientId
6889
- };
6890
- if (opts.clientSecret) body.client_secret = opts.clientSecret;
6891
- if (opts.scope) body.scope = opts.scope;
7786
+ client_id: opts.clientId,
7787
+ ...opts.clientSecret ? { client_secret: opts.clientSecret } : {},
7788
+ ...opts.scope ? { scope: opts.scope } : {}
7789
+ });
6892
7790
  const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/oauth/token`, {
6893
7791
  method: "POST",
6894
7792
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
6895
- body: new URLSearchParams(body).toString()
7793
+ body
6896
7794
  });
6897
7795
  if (!res.ok) throw await oauthTokenError(res, "oauth.refreshAccessToken");
6898
- return await res.json();
7796
+ return decodeOAuthTokenResult(await res.json());
6899
7797
  },
6900
- /**
6901
- * Poll the OAuth token endpoint for a device-code grant (ADR-089 Phase
6902
- * 5.1c — RFC 8628 §3.4). The preferred way to exchange an approved
6903
- * device grant for tokens — returns an RFC 6749 error envelope on the
6904
- * `{pending, slow_down, denied, expired}` states so callers can
6905
- * distinguish precisely without parsing Phase 2a's `/auth/device/poll`
6906
- * status string.
6907
- *
6908
- * Returns `{ ok: true, tokens }` on success or `{ ok: false, error }`
6909
- * for every RFC-defined polling outcome. Callers MUST honour the
6910
- * polling `interval` returned by `/auth/device` — polling faster yields
6911
- * `{ ok: false, error: 'slow_down' }`.
6912
- *
6913
- * @example
6914
- * ```typescript
6915
- * while (true) {
6916
- * await sleep(interval * 1000)
6917
- * const r = await auth.oauth.pollDeviceToken({
6918
- * baseUrl: 'https://api.sylphx.com/v1',
6919
- * clientId: 'sylphx-cli',
6920
- * deviceCode,
6921
- * })
6922
- * if (r.ok) return r.tokens
6923
- * if (r.error === 'authorization_pending' || r.error === 'slow_down') continue
6924
- * throw new Error(r.error) // access_denied | expired_token
6925
- * }
6926
- * ```
6927
- */
6928
7798
  async pollDeviceToken(opts) {
6929
- const body = new URLSearchParams({
7799
+ const body = oauthTokenFormBody({
6930
7800
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
6931
7801
  device_code: opts.deviceCode,
6932
7802
  client_id: opts.clientId
6933
- }).toString();
7803
+ });
6934
7804
  const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/oauth/token`, {
6935
7805
  method: "POST",
6936
7806
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
6937
7807
  body
6938
7808
  });
6939
- if (res.ok) {
6940
- return { ok: true, tokens: await res.json() };
6941
- }
7809
+ if (res.ok) return { ok: true, tokens: decodeOAuthTokenResult(await res.json()) };
6942
7810
  const text = await res.text().catch(() => "");
6943
7811
  let code = "oauth_error";
6944
7812
  try {
6945
- const parsed = JSON.parse(text);
6946
- if (parsed.error) code = parsed.error;
7813
+ code = decodeOAuthTokenError(JSON.parse(text)).error;
6947
7814
  } catch {
6948
7815
  }
6949
7816
  return { ok: false, error: code, status: res.status };
6950
7817
  },
6951
- /**
6952
- * Mint a service-principal access token via the `client_credentials`
6953
- * grant (ADR-089 Phase 5.1c — RFC 6749 §4.4). Requires a confidential
6954
- * client (public clients cannot use this grant). No refresh token is
6955
- * issued per §4.4.3 — callers re-run this exchange on expiry.
6956
- *
6957
- * Typical use: CI integrations, server-to-server automation that has
6958
- * no human owner and cannot run a device flow.
6959
- *
6960
- * @example
6961
- * ```typescript
6962
- * const { access_token } = await auth.oauth.clientCredentialsToken({
6963
- * baseUrl: 'https://api.sylphx.com/v1',
6964
- * clientId: process.env.SYLPHX_CLIENT_ID!,
6965
- * clientSecret: process.env.SYLPHX_CLIENT_SECRET!,
6966
- * scope: 'tenants:provision',
6967
- * })
6968
- * ```
6969
- */
6970
7818
  async clientCredentialsToken(opts) {
6971
- const body = {
7819
+ const body = oauthTokenFormBody({
6972
7820
  grant_type: "client_credentials",
6973
7821
  client_id: opts.clientId,
6974
- client_secret: opts.clientSecret
6975
- };
6976
- if (opts.scope) body.scope = opts.scope;
7822
+ client_secret: opts.clientSecret,
7823
+ ...opts.scope ? { scope: opts.scope } : {}
7824
+ });
6977
7825
  const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/oauth/token`, {
6978
7826
  method: "POST",
6979
7827
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
6980
- body: new URLSearchParams(body).toString()
7828
+ body
6981
7829
  });
6982
7830
  if (!res.ok) throw await oauthTokenError(res, "oauth.clientCredentialsToken");
6983
- return await res.json();
7831
+ return decodeOAuthClientCredentialsResult(await res.json());
6984
7832
  },
6985
- /**
6986
- * Revoke an OAuth access or refresh token (RFC 7009 — ADR-089 Phase 5.1d).
6987
- *
6988
- * Per §2.2 this always resolves successfully — the server returns 200
6989
- * whether the token existed, was already revoked, or belonged to a
6990
- * different client. Only true protocol-level failures (malformed
6991
- * request, bad client credentials) throw.
6992
- *
6993
- * @example
6994
- * ```typescript
6995
- * await auth.oauth.revokeToken({
6996
- * baseUrl: 'https://your-app.api.sylphx.com/v1',
6997
- * clientId: 'sylphx-cli',
6998
- * token: refreshToken,
6999
- * tokenTypeHint: 'refresh_token',
7000
- * })
7001
- * ```
7002
- */
7003
7833
  async revokeToken(opts) {
7004
7834
  const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/oauth/revoke`, {
7005
7835
  method: "POST",
7006
- headers: buildDeviceHeaders(opts.userAgent),
7836
+ headers: buildJsonHeaders(opts.userAgent),
7007
7837
  body: JSON.stringify({
7008
7838
  token: opts.token,
7009
7839
  token_type_hint: opts.tokenTypeHint,
@@ -7016,29 +7846,10 @@ var oauth = {
7016
7846
  throw new Error(`oauth.revokeToken failed (${res.status}): ${body}`);
7017
7847
  }
7018
7848
  },
7019
- /**
7020
- * Introspect an OAuth access or refresh token (RFC 7662 — ADR-089 Phase 5.1d).
7021
- *
7022
- * Returns `{ active: false }` for expired / revoked / unknown /
7023
- * not-owned tokens (without revealing which); `{ active: true, ... }`
7024
- * with full claims for live ones. Only protocol-level failures
7025
- * (4xx on the revocation envelope itself) throw.
7026
- *
7027
- * @example
7028
- * ```typescript
7029
- * const result = await auth.oauth.introspectToken({
7030
- * baseUrl: 'https://your-app.api.sylphx.com/v1',
7031
- * clientId: 'gateway',
7032
- * clientSecret: process.env.GATEWAY_SECRET,
7033
- * token: accessToken,
7034
- * })
7035
- * if (!result.active) throw new Error('token not accepted')
7036
- * ```
7037
- */
7038
7849
  async introspectToken(opts) {
7039
7850
  const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/oauth/introspect`, {
7040
7851
  method: "POST",
7041
- headers: buildDeviceHeaders(opts.userAgent),
7852
+ headers: buildJsonHeaders(opts.userAgent),
7042
7853
  body: JSON.stringify({
7043
7854
  token: opts.token,
7044
7855
  token_type_hint: opts.tokenTypeHint,
@@ -7051,318 +7862,572 @@ var oauth = {
7051
7862
  throw new Error(`oauth.introspectToken failed (${res.status}): ${body}`);
7052
7863
  }
7053
7864
  return await res.json();
7054
- }
7055
- };
7056
- var dpop = {
7057
- /**
7058
- * Generate a fresh ES256 key pair. Private key is non-extractable
7059
- * (`extractable: false`) so it can be stored but never serialised —
7060
- * the only legal operation is `sign`. Clients that need to
7061
- * hibernate the keypair across restarts must use a host-provided
7062
- * secure store (Keychain, Credential Manager, IndexedDB + CryptoKey
7063
- * wrapping).
7064
- */
7065
- async generateKeyPair() {
7066
- const { privateKey, publicKey } = await crypto.subtle.generateKey(
7067
- { name: "ECDSA", namedCurve: "P-256" },
7068
- false,
7069
- ["sign", "verify"]
7070
- );
7071
- const publicJwk = await crypto.subtle.exportKey("jwk", publicKey);
7072
- const thumbprint = await thumbprintFromJwk(sanitisePublicJwk(publicJwk));
7073
- return { privateKey, publicKey, thumbprint };
7865
+ }
7866
+ };
7867
+ function buildJsonHeaders(userAgent) {
7868
+ return {
7869
+ "Content-Type": "application/json",
7870
+ ...userAgent ? { "User-Agent": userAgent } : {}
7871
+ };
7872
+ }
7873
+ async function platformJwtError(res, operation) {
7874
+ const body = await res.text().catch(() => "");
7875
+ let code = "platform_jwt_error";
7876
+ let message2 = `${operation} failed: HTTP ${res.status}`;
7877
+ try {
7878
+ const parsed = JSON.parse(body);
7879
+ if (parsed.error) code = parsed.error;
7880
+ if (parsed.error_description) message2 = parsed.error_description;
7881
+ else if (parsed.message) message2 = parsed.message;
7882
+ } catch {
7883
+ }
7884
+ let errorCode;
7885
+ if (res.status === 401) errorCode = "UNAUTHORIZED";
7886
+ else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
7887
+ else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
7888
+ else errorCode = "BAD_REQUEST";
7889
+ return new SylphxError(message2, {
7890
+ code: errorCode,
7891
+ status: res.status,
7892
+ data: { operation, code }
7893
+ });
7894
+ }
7895
+
7896
+ // src/platform-password.ts
7897
+ init_errors();
7898
+ var password = {
7899
+ async status(opts) {
7900
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-password/status`, {
7901
+ method: "GET",
7902
+ headers: buildPlatformPasswordHeaders(opts.accessToken, opts.userAgent)
7903
+ });
7904
+ if (!res.ok) throw await platformPasswordError(res, "password.status");
7905
+ return await res.json();
7906
+ },
7907
+ async set(opts) {
7908
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-password/set`, {
7909
+ method: "POST",
7910
+ headers: buildPlatformPasswordHeaders(opts.accessToken, opts.userAgent),
7911
+ body: JSON.stringify({
7912
+ password: opts.password
7913
+ })
7914
+ });
7915
+ if (!res.ok) throw await platformPasswordError(res, "password.set");
7916
+ return await res.json();
7917
+ },
7918
+ async change(opts) {
7919
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-password/change`, {
7920
+ method: "POST",
7921
+ headers: buildPlatformPasswordHeaders(opts.accessToken, opts.userAgent),
7922
+ body: JSON.stringify({
7923
+ currentPassword: opts.currentPassword,
7924
+ newPassword: opts.newPassword
7925
+ })
7926
+ });
7927
+ if (!res.ok) throw await platformPasswordError(res, "password.change");
7928
+ return await res.json();
7929
+ }
7930
+ };
7931
+ function buildPlatformPasswordHeaders(accessToken, userAgent) {
7932
+ const headers = {
7933
+ "Content-Type": "application/json",
7934
+ Authorization: `Bearer ${accessToken}`
7935
+ };
7936
+ if (userAgent) headers["User-Agent"] = userAgent;
7937
+ return headers;
7938
+ }
7939
+ async function platformPasswordError(res, operation) {
7940
+ const body = await res.text().catch(() => "");
7941
+ let code = "platform_password_error";
7942
+ let message2 = `${operation} failed: HTTP ${res.status}`;
7943
+ try {
7944
+ const parsed = JSON.parse(body);
7945
+ if (parsed.error) code = parsed.error;
7946
+ if (parsed.error_description) message2 = parsed.error_description;
7947
+ else if (parsed.message) message2 = parsed.message;
7948
+ } catch {
7949
+ }
7950
+ let errorCode;
7951
+ if (res.status === 401) errorCode = "UNAUTHORIZED";
7952
+ else if (res.status === 404) errorCode = "NOT_FOUND";
7953
+ else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
7954
+ else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
7955
+ else errorCode = "BAD_REQUEST";
7956
+ return new SylphxError(message2, {
7957
+ code: errorCode,
7958
+ status: res.status,
7959
+ data: { operation, code }
7960
+ });
7961
+ }
7962
+
7963
+ // src/platform-sessions.ts
7964
+ init_errors();
7965
+ var sessions = {
7966
+ async list(opts) {
7967
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions`, {
7968
+ method: "GET",
7969
+ headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent)
7970
+ });
7971
+ if (!res.ok) throw await platformSessionError(res, "sessions.list");
7972
+ return await res.json();
7973
+ },
7974
+ async revoke(opts) {
7975
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/revoke`, {
7976
+ method: "POST",
7977
+ headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent),
7978
+ body: JSON.stringify({ sessionId: opts.sessionId })
7979
+ });
7980
+ if (!res.ok) throw await platformSessionError(res, "sessions.revoke");
7981
+ return await res.json();
7982
+ },
7983
+ async revokeOther(opts) {
7984
+ const res = await fetch(
7985
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/revoke-other`,
7986
+ {
7987
+ method: "POST",
7988
+ headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent)
7989
+ }
7990
+ );
7991
+ if (!res.ok) throw await platformSessionError(res, "sessions.revokeOther");
7992
+ return await res.json();
7993
+ },
7994
+ async revokeAll(opts) {
7995
+ const res = await fetch(
7996
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/revoke-all`,
7997
+ {
7998
+ method: "POST",
7999
+ headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent)
8000
+ }
8001
+ );
8002
+ if (!res.ok) throw await platformSessionError(res, "sessions.revokeAll");
8003
+ return await res.json();
8004
+ },
8005
+ async rename(opts) {
8006
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-sessions/rename`, {
8007
+ method: "POST",
8008
+ headers: buildPlatformSessionsHeaders(opts.accessToken, opts.userAgent),
8009
+ body: JSON.stringify({
8010
+ sessionId: opts.sessionId,
8011
+ name: opts.name
8012
+ })
8013
+ });
8014
+ if (!res.ok) throw await platformSessionError(res, "sessions.rename");
8015
+ return await res.json();
8016
+ }
8017
+ };
8018
+ function buildPlatformSessionsHeaders(accessToken, userAgent) {
8019
+ const headers = {
8020
+ "Content-Type": "application/json",
8021
+ Authorization: `Bearer ${accessToken}`
8022
+ };
8023
+ if (userAgent) headers["User-Agent"] = userAgent;
8024
+ return headers;
8025
+ }
8026
+ async function platformSessionError(res, operation) {
8027
+ const body = await res.text().catch(() => "");
8028
+ let code = "platform_sessions_error";
8029
+ let message2 = `${operation} failed: HTTP ${res.status}`;
8030
+ try {
8031
+ const parsed = JSON.parse(body);
8032
+ if (parsed.error) code = parsed.error;
8033
+ if (parsed.error_description) message2 = parsed.error_description;
8034
+ else if (parsed.message) message2 = parsed.message;
8035
+ } catch {
8036
+ }
8037
+ let errorCode;
8038
+ if (res.status === 401) errorCode = "UNAUTHORIZED";
8039
+ else if (res.status === 404) errorCode = "NOT_FOUND";
8040
+ else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
8041
+ else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
8042
+ else errorCode = "BAD_REQUEST";
8043
+ return new SylphxError(message2, {
8044
+ code: errorCode,
8045
+ status: res.status,
8046
+ data: { operation, code }
8047
+ });
8048
+ }
8049
+
8050
+ // src/platform-user.ts
8051
+ init_errors();
8052
+ var user = {
8053
+ /**
8054
+ * Export every piece of personal data the platform holds about the
8055
+ * authenticated user (GDPR Article 20 — right to data portability).
8056
+ *
8057
+ * The returned record is deliberately loose — it contains the user
8058
+ * row, sessions, OAuth accounts, login history, security alerts,
8059
+ * organization memberships, subscriptions, per-project memberships,
8060
+ * and storage file metadata. Shape varies with customer provisioning.
8061
+ */
8062
+ async exportData(opts) {
8063
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export`, {
8064
+ method: "GET",
8065
+ headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent)
8066
+ });
8067
+ if (!res.ok) throw await platformUserError(res, "user.exportData");
8068
+ return await res.json();
8069
+ },
8070
+ /**
8071
+ * Permanently delete the authenticated user's account (GDPR Article
8072
+ * 17 — right to erasure). Cascades through every provisioned project
8073
+ * DB, cancels Stripe subscriptions, deletes S3 blobs, and anonymises
8074
+ * billing transactions.
8075
+ */
8076
+ async deleteAccount(opts) {
8077
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/account`, {
8078
+ method: "DELETE",
8079
+ headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent),
8080
+ body: JSON.stringify({
8081
+ ...opts.reason !== void 0 && { reason: opts.reason }
8082
+ })
8083
+ });
8084
+ if (!res.ok) throw await platformUserError(res, "user.deleteAccount");
8085
+ return await res.json();
7074
8086
  },
7075
8087
  /**
7076
- * Sign a DPoP proof JWT. When `accessToken` is provided, the proof
7077
- * includes `ath = base64url(sha256(accessToken))` so the resource
7078
- * server can bind the proof to the token being presented (RFC 9449
7079
- * §4.3 step 11).
8088
+ * Async GDPR Article 20 export job API (ADR-089 Phase 5.5).
8089
+ *
8090
+ * `user.exportData` is the Phase 2d synchronous shortcut; production
8091
+ * callers should prefer the async flow for large accounts.
7080
8092
  */
7081
- async generateProof(opts) {
7082
- const publicJwkRaw = await crypto.subtle.exportKey("jwk", opts.publicKey);
7083
- const publicJwk = sanitisePublicJwk(publicJwkRaw);
7084
- const header = { typ: "dpop+jwt", alg: "ES256", jwk: publicJwk };
7085
- const payload = {
7086
- jti: randomJti(),
7087
- htm: opts.method.toUpperCase(),
7088
- htu: stripQueryAndFragment(opts.uri),
7089
- iat: Math.floor(Date.now() / 1e3)
7090
- };
7091
- if (opts.accessToken) {
7092
- payload.ath = await sha256Base64Url(new TextEncoder().encode(opts.accessToken));
8093
+ exports: {
8094
+ /**
8095
+ * Kick off an export job. Poll `status({ id })` until
8096
+ * `status === 'complete'`.
8097
+ */
8098
+ async initiate(opts) {
8099
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export`, {
8100
+ method: "POST",
8101
+ headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent),
8102
+ body: JSON.stringify(opts.format !== void 0 ? { format: opts.format } : {})
8103
+ });
8104
+ if (!res.ok) throw await platformUserError(res, "user.exports.initiate");
8105
+ return await res.json();
8106
+ },
8107
+ /**
8108
+ * Read the current state of an in-flight or completed export job.
8109
+ */
8110
+ async status(opts) {
8111
+ const res = await fetch(
8112
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export/${encodeURIComponent(
8113
+ opts.id
8114
+ )}`,
8115
+ {
8116
+ method: "GET",
8117
+ headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent)
8118
+ }
8119
+ );
8120
+ if (!res.ok) throw await platformUserError(res, "user.exports.status");
8121
+ return await res.json();
8122
+ },
8123
+ /**
8124
+ * Download the completed export payload. The BaaS route returns a
8125
+ * 302 to a freshly-signed object-storage URL; `fetch` follows it
8126
+ * and resolves to the raw `Blob`.
8127
+ */
8128
+ async download(opts) {
8129
+ const res = await fetch(
8130
+ `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-user/export/${encodeURIComponent(
8131
+ opts.id
8132
+ )}/download`,
8133
+ {
8134
+ method: "GET",
8135
+ headers: buildPlatformUserHeaders(opts.accessToken, opts.userAgent)
8136
+ }
8137
+ );
8138
+ if (!res.ok) throw await platformUserError(res, "user.exports.download");
8139
+ const sha256 = res.headers.get("X-Sylphx-Export-Sha256");
8140
+ const sizeHeader = res.headers.get("X-Sylphx-Export-Size");
8141
+ const sizeBytes = sizeHeader ? Number.parseInt(sizeHeader, 10) : null;
8142
+ const blob = await res.blob();
8143
+ return { blob, sha256, sizeBytes: Number.isFinite(sizeBytes) ? sizeBytes : null };
7093
8144
  }
7094
- if (opts.nonce) payload.nonce = opts.nonce;
7095
- const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)));
7096
- const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)));
7097
- const signingInput = `${headerB64}.${payloadB64}`;
7098
- const signingBytes = new TextEncoder().encode(signingInput);
7099
- const signingBuf = new Uint8Array(signingBytes.byteLength);
7100
- signingBuf.set(signingBytes);
7101
- const sigBuf = await crypto.subtle.sign(
7102
- { name: "ECDSA", hash: "SHA-256" },
7103
- opts.privateKey,
7104
- signingBuf.buffer
7105
- );
7106
- const sigB64 = base64UrlEncode(new Uint8Array(sigBuf));
7107
- return `${signingInput}.${sigB64}`;
7108
8145
  }
7109
8146
  };
7110
- function sanitisePublicJwk(jwk) {
7111
- if (jwk.kty !== "EC" || jwk.crv !== "P-256" || !jwk.x || !jwk.y) {
7112
- throw new Error("DPoP expects ES256 (EC P-256) JWK");
7113
- }
7114
- return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
7115
- }
7116
- async function thumbprintFromJwk(jwk) {
7117
- const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
7118
- return sha256Base64Url(new TextEncoder().encode(canonical));
7119
- }
7120
- async function sha256Base64Url(data) {
7121
- const copy = new Uint8Array(data.byteLength);
7122
- copy.set(data);
7123
- const digest2 = await crypto.subtle.digest("SHA-256", copy.buffer);
7124
- return base64UrlEncode(new Uint8Array(digest2));
7125
- }
7126
- function base64UrlEncode(bytes) {
7127
- let s = "";
7128
- for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
7129
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7130
- }
7131
- function stripQueryAndFragment(uri) {
7132
- try {
7133
- const u = new URL(uri);
7134
- return `${u.protocol}//${u.host}${u.pathname}`;
7135
- } catch {
7136
- const q = uri.indexOf("?");
7137
- const h = uri.indexOf("#");
7138
- const cut = [q, h].filter((i) => i >= 0).sort((a, b) => a - b)[0];
7139
- return cut === void 0 ? uri : uri.slice(0, cut);
7140
- }
7141
- }
7142
- function randomJti() {
7143
- const bytes = new Uint8Array(16);
7144
- crypto.getRandomValues(bytes);
7145
- return base64UrlEncode(bytes);
8147
+ function buildPlatformUserHeaders(accessToken, userAgent) {
8148
+ const headers = {
8149
+ "Content-Type": "application/json",
8150
+ Authorization: `Bearer ${accessToken}`
8151
+ };
8152
+ if (userAgent) headers["User-Agent"] = userAgent;
8153
+ return headers;
7146
8154
  }
7147
- async function oauthTokenError(res, operation) {
7148
- const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
8155
+ async function platformUserError(res, operation) {
7149
8156
  const body = await res.text().catch(() => "");
7150
- let code = "oauth_error";
8157
+ let code = "platform_user_error";
7151
8158
  let message2 = `${operation} failed: HTTP ${res.status}`;
7152
8159
  try {
7153
8160
  const parsed = JSON.parse(body);
7154
8161
  if (parsed.error) code = parsed.error;
7155
8162
  if (parsed.error_description) message2 = parsed.error_description;
8163
+ else if (parsed.message) message2 = parsed.message;
7156
8164
  } catch {
7157
8165
  }
7158
8166
  let errorCode;
7159
8167
  if (res.status === 401) errorCode = "UNAUTHORIZED";
8168
+ else if (res.status === 404) errorCode = "NOT_FOUND";
8169
+ else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
7160
8170
  else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
7161
8171
  else errorCode = "BAD_REQUEST";
7162
- return new SylphxError2(message2, {
8172
+ return new SylphxError(message2, {
7163
8173
  code: errorCode,
7164
8174
  status: res.status,
7165
- data: { oauthError: code }
8175
+ data: { operation, code }
8176
+ });
8177
+ }
8178
+
8179
+ // src/auth.ts
8180
+ async function signIn(config, input) {
8181
+ const body = input;
8182
+ const endpoint = authEndpoints.signIn;
8183
+ return callApi(config, endpoint.path, {
8184
+ method: endpoint.method,
8185
+ body
8186
+ });
8187
+ }
8188
+ async function signUp(config, input) {
8189
+ const endpoint = authEndpoints.signUp;
8190
+ return callApi(config, endpoint.path, {
8191
+ method: endpoint.method,
8192
+ body: input
8193
+ });
8194
+ }
8195
+ async function signOut(config) {
8196
+ const endpoint = authEndpoints.signOut;
8197
+ await callApi(config, endpoint.path, { method: endpoint.method });
8198
+ }
8199
+ async function refreshToken(config, token) {
8200
+ return callApi(config, "/auth/token", {
8201
+ method: "POST",
8202
+ body: {
8203
+ grant_type: "refresh_token",
8204
+ refresh_token: token,
8205
+ client_secret: config.secretKey
8206
+ }
8207
+ });
8208
+ }
8209
+ async function verifyEmail(config, token) {
8210
+ await callApi(config, "/auth/verify-email", {
8211
+ method: "POST",
8212
+ body: { token }
8213
+ });
8214
+ }
8215
+ async function forgotPassword(config, email, options = {}) {
8216
+ await callApi(config, "/auth/forgot-password", {
8217
+ method: "POST",
8218
+ body: {
8219
+ email,
8220
+ ...options.redirectUrl ? { redirectUrl: options.redirectUrl } : {}
8221
+ }
8222
+ });
8223
+ }
8224
+ async function resendVerificationEmail(config, email) {
8225
+ const endpoint = authEndpoints.resendEmailVerification;
8226
+ const body = { email };
8227
+ await callApi(config, endpoint.path, {
8228
+ method: endpoint.method,
8229
+ body
8230
+ });
8231
+ }
8232
+ async function resetPassword(config, input) {
8233
+ await callApi(config, "/auth/reset-password", {
8234
+ method: "POST",
8235
+ body: { token: input.token, password: input.password }
8236
+ });
8237
+ }
8238
+ async function getSession(config) {
8239
+ if (!config.accessToken) {
8240
+ return { user: null };
8241
+ }
8242
+ const endpoint = authEndpoints.getSession;
8243
+ try {
8244
+ const user2 = await callApi(config, endpoint.path, {
8245
+ method: endpoint.method
8246
+ });
8247
+ return { user: user2 };
8248
+ } catch {
8249
+ return { user: null };
8250
+ }
8251
+ }
8252
+ async function verifyTwoFactor(config, userId, code) {
8253
+ return callApi(config, "/auth/verify-2fa", {
8254
+ method: "POST",
8255
+ body: { userId, code }
8256
+ });
8257
+ }
8258
+ async function introspectToken(config, token, tokenTypeHint) {
8259
+ const response = await fetch(buildApiUrl(config, "/auth/introspect"), {
8260
+ method: "POST",
8261
+ headers: {
8262
+ "Content-Type": "application/json",
8263
+ // RFC 7662 §2: server-to-server call — authenticate with secret key
8264
+ "x-app-secret": config.secretKey ?? ""
8265
+ },
8266
+ body: JSON.stringify({
8267
+ token,
8268
+ token_type_hint: tokenTypeHint
8269
+ })
8270
+ });
8271
+ if (!response.ok) {
8272
+ return { active: false };
8273
+ }
8274
+ return response.json();
8275
+ }
8276
+ async function revokeToken(config, token, options) {
8277
+ await fetch(buildApiUrl(config, "/auth/revoke"), {
8278
+ method: "POST",
8279
+ headers: { "Content-Type": "application/json" },
8280
+ body: JSON.stringify({
8281
+ token: options?.revokeAll ? void 0 : token,
8282
+ client_secret: config.secretKey,
8283
+ user_id: options?.userId,
8284
+ revoke_all: options?.revokeAll
8285
+ })
7166
8286
  });
7167
8287
  }
7168
- async function platformJwtError(res, operation) {
7169
- const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
7170
- const body = await res.text().catch(() => "");
7171
- let code = "platform_jwt_error";
7172
- let message2 = `${operation} failed: HTTP ${res.status}`;
7173
- try {
7174
- const parsed = JSON.parse(body);
7175
- if (parsed.error) code = parsed.error;
7176
- if (parsed.error_description) message2 = parsed.error_description;
7177
- else if (parsed.message) message2 = parsed.message;
7178
- } catch {
8288
+ async function revokeAllTokens(config, userId) {
8289
+ await revokeToken(config, "", { revokeAll: true, userId });
8290
+ }
8291
+ async function extendedSignUp(config, input) {
8292
+ return callApi(config, "/auth/register", {
8293
+ method: "POST",
8294
+ body: input
8295
+ });
8296
+ }
8297
+ async function inviteUser(config, input) {
8298
+ return callApi(config, "/auth/invite", {
8299
+ method: "POST",
8300
+ body: input
8301
+ });
8302
+ }
8303
+ function normalizeOrgScopedTokenResponse(data) {
8304
+ const accessToken = data.accessToken ?? data.access_token;
8305
+ if (!accessToken) {
8306
+ throw new Error("Invalid org-scoped token response: missing access token");
7179
8307
  }
7180
- let errorCode;
7181
- if (res.status === 401) errorCode = "UNAUTHORIZED";
7182
- else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
7183
- else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
7184
- else errorCode = "BAD_REQUEST";
7185
- return new SylphxError2(message2, {
7186
- code: errorCode,
7187
- status: res.status,
7188
- data: { operation, code }
8308
+ return {
8309
+ token: accessToken,
8310
+ accessToken,
8311
+ expiresIn: data.expiresIn ?? data.expires_in,
8312
+ tokenType: data.tokenType ?? data.token_type,
8313
+ user: data.user
8314
+ };
8315
+ }
8316
+ async function getOrgScopedToken(config, orgId) {
8317
+ const data = await callApi(config, "/auth/switch-org", {
8318
+ method: "POST",
8319
+ body: { orgId }
7189
8320
  });
8321
+ return normalizeOrgScopedTokenResponse(data);
7190
8322
  }
7191
- var impersonation = {
7192
- async start(opts) {
7193
- const res = await fetch(
7194
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/start`,
7195
- {
7196
- method: "POST",
7197
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7198
- body: JSON.stringify({
7199
- targetUserId: opts.targetUserId,
7200
- ...opts.ipAddress !== void 0 && { ipAddress: opts.ipAddress },
7201
- ...opts.userAgent !== void 0 && { userAgent: opts.userAgent }
7202
- })
7203
- }
7204
- );
7205
- if (!res.ok) throw await impersonationError(res, "impersonation.start");
7206
- return await res.json();
7207
- },
7208
- async end(opts) {
7209
- const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/end`, {
7210
- method: "POST",
7211
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7212
- body: JSON.stringify(opts.sessionId !== void 0 ? { sessionId: opts.sessionId } : {})
7213
- });
7214
- if (!res.ok) throw await impersonationError(res, "impersonation.end");
7215
- return await res.json();
7216
- },
7217
- async info(opts) {
7218
- const res = await fetch(
7219
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/info/${encodeURIComponent(opts.sessionId)}`,
7220
- {
7221
- method: "GET",
7222
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7223
- }
7224
- );
7225
- if (!res.ok) throw await impersonationError(res, "impersonation.info");
7226
- return await res.json();
7227
- },
7228
- async active(opts) {
7229
- const res = await fetch(
7230
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/active`,
7231
- {
7232
- method: "GET",
7233
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7234
- }
7235
- );
7236
- if (!res.ok) throw await impersonationError(res, "impersonation.active");
7237
- return await res.json();
7238
- },
7239
- // ── Phase 5.9 ──────────────────────────────────────────────────────
8323
+ async function switchOrg(config, orgId) {
8324
+ return getOrgScopedToken(config, orgId);
8325
+ }
8326
+ var device = {
7240
8327
  /**
7241
- * Phase 5.9 step 1 of 2 — request a WebAuthn assertion challenge.
7242
- * Returns the pending-request id plus options ready for
7243
- * `navigator.credentials.get(...)`. Caller is expected to post the
7244
- * resulting assertion to {@link impersonation.startStepup}.
8328
+ * Start a device authorization grant.
8329
+ *
8330
+ * Returns a `DeviceGrant` with `verification_uri_complete` (open this
8331
+ * in the user's browser) and `device_code` (use for polling).
8332
+ *
8333
+ * @example
8334
+ * ```typescript
8335
+ * const grant = await device.init({
8336
+ * baseUrl: 'https://your-app.api.sylphx.com/v1',
8337
+ * clientId: 'sylphx-cli',
8338
+ * scope: ['org:read', 'project:*'],
8339
+ * })
8340
+ * openBrowser(grant.verification_uri_complete)
8341
+ * ```
7245
8342
  */
7246
- async startChallenge(opts) {
7247
- const res = await fetch(
7248
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/start-challenge`,
7249
- {
7250
- method: "POST",
7251
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7252
- body: JSON.stringify({
7253
- targetUserId: opts.targetUserId,
7254
- reason: opts.reason
7255
- })
7256
- }
7257
- );
7258
- if (!res.ok) throw await impersonationError(res, "impersonation.startChallenge");
8343
+ async init(opts) {
8344
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/device`, {
8345
+ method: "POST",
8346
+ headers: buildDeviceHeaders(opts.userAgent),
8347
+ body: JSON.stringify({
8348
+ client_id: opts.clientId,
8349
+ scope: opts.scope ?? []
8350
+ })
8351
+ });
8352
+ if (!res.ok) throw await deviceError(res, "device.init");
7259
8353
  return await res.json();
7260
8354
  },
7261
8355
  /**
7262
- * Phase 5.9 step 2 of 2 complete the WebAuthn step-up. Returns
7263
- * either the active session (emergency bypass) or the
7264
- * consent deadline (regular flow). Phase 3b `start` is superseded
7265
- * by this method; old callers should migrate to
7266
- * `startChallenge` `startStepup`.
8356
+ * Poll a pending grant. Returns `status: 'pending' | 'approved' |
8357
+ * 'denied' | 'expired'`. On `approved`, the result carries the OAuth
8358
+ * pair (access_token + refresh_token).
8359
+ *
8360
+ * Callers MUST respect the `interval` returned by `init()` — polling
8361
+ * faster than that may return 429 slow_down (RFC 8628 §5.5).
7267
8362
  */
7268
- async startStepup(opts) {
7269
- const res = await fetch(
7270
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/start`,
7271
- {
7272
- method: "POST",
7273
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7274
- body: JSON.stringify({
7275
- requestId: opts.requestId,
7276
- challengeKey: opts.challengeKey,
7277
- assertion: opts.assertion,
7278
- ...opts.emergencyBypass ? { emergencyBypass: true } : {}
7279
- })
7280
- }
7281
- );
7282
- if (!res.ok) throw await impersonationError(res, "impersonation.startStepup");
8363
+ async poll(opts) {
8364
+ const url = new URL(`${opts.baseUrl.replace(/\/$/, "")}/auth/device/poll`);
8365
+ url.searchParams.set("device_code", opts.deviceCode);
8366
+ const res = await fetch(url.toString(), {
8367
+ method: "GET",
8368
+ headers: buildDeviceHeaders(opts.userAgent)
8369
+ });
8370
+ if (!res.ok) throw await deviceError(res, "device.poll");
7283
8371
  return await res.json();
7284
8372
  },
7285
8373
  /**
7286
- * Target user's consent decision. Approve mints the session token;
7287
- * deny transitions the request to `denied`.
8374
+ * Browser leg the approving user confirms the grant.
8375
+ *
8376
+ * Requires a valid platform-issued access token (`Authorization:
8377
+ * Bearer <accessToken>`) proving the user is logged in on the
8378
+ * Console. Typically called by the Console's `/device` verification
8379
+ * page server-side, forwarding the user's session JWT.
7288
8380
  */
7289
- async respondConsent(opts) {
7290
- const res = await fetch(
7291
- `${opts.baseUrl.replace(/\/$/, "")}/auth/impersonation-consent/${encodeURIComponent(opts.requestId)}`,
7292
- {
7293
- method: "POST",
7294
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent),
7295
- body: JSON.stringify({ decision: opts.decision })
7296
- }
7297
- );
7298
- if (!res.ok) throw await impersonationError(res, "impersonation.respondConsent");
7299
- return await res.json();
7300
- },
7301
- /** List impersonation requests. Non-super_admin sees only their own. */
7302
- async listRequests(opts) {
7303
- const params = new URLSearchParams();
7304
- if (opts.filter?.operatorId) params.set("operatorId", opts.filter.operatorId);
7305
- if (opts.filter?.targetUserId) params.set("targetUserId", opts.filter.targetUserId);
7306
- if (opts.filter?.status) params.set("status", opts.filter.status);
7307
- if (opts.filter?.limit != null) params.set("limit", String(opts.filter.limit));
7308
- const qs = params.toString();
7309
- const res = await fetch(
7310
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/requests${qs ? `?${qs}` : ""}`,
7311
- {
7312
- method: "GET",
7313
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7314
- }
7315
- );
7316
- if (!res.ok) throw await impersonationError(res, "impersonation.listRequests");
8381
+ async approve(opts) {
8382
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/device/approve`, {
8383
+ method: "POST",
8384
+ headers: {
8385
+ ...buildDeviceHeaders(opts.userAgent),
8386
+ Authorization: `Bearer ${opts.accessToken}`
8387
+ },
8388
+ body: JSON.stringify({ user_code: opts.userCode })
8389
+ });
8390
+ if (!res.ok) throw await deviceError(res, "device.approve");
7317
8391
  return await res.json();
7318
8392
  },
7319
8393
  /**
7320
- * End an active impersonation session by request id. Emits a CAEP
7321
- * `session-revoked` event via the Phase 5.Z outbox so every in-flight
7322
- * verifier invalidates the token within ≤1s.
8394
+ * Browser leg the user declines the grant.
8395
+ *
8396
+ * Requires a valid platform-issued access token just like `approve`.
7323
8397
  */
7324
- async endSession(opts) {
7325
- const res = await fetch(
7326
- `${opts.baseUrl.replace(/\/$/, "")}/auth/platform-impersonation/end/${encodeURIComponent(opts.requestId)}`,
7327
- {
7328
- method: "POST",
7329
- headers: buildImpersonationHeaders(opts.accessToken, opts.userAgent)
7330
- }
7331
- );
7332
- if (!res.ok) throw await impersonationError(res, "impersonation.endSession");
8398
+ async deny(opts) {
8399
+ const res = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/auth/device/deny`, {
8400
+ method: "POST",
8401
+ headers: {
8402
+ ...buildDeviceHeaders(opts.userAgent),
8403
+ Authorization: `Bearer ${opts.accessToken}`
8404
+ },
8405
+ body: JSON.stringify({ user_code: opts.userCode })
8406
+ });
8407
+ if (!res.ok) throw await deviceError(res, "device.deny");
7333
8408
  return await res.json();
7334
8409
  }
7335
8410
  };
7336
- function buildImpersonationHeaders(accessToken, userAgent) {
8411
+ function buildDeviceHeaders(userAgent) {
7337
8412
  const headers = {
7338
- "Content-Type": "application/json",
7339
- Authorization: `Bearer ${accessToken}`
8413
+ "Content-Type": "application/json"
7340
8414
  };
7341
8415
  if (userAgent) headers["User-Agent"] = userAgent;
7342
8416
  return headers;
7343
8417
  }
7344
- async function impersonationError(res, operation) {
8418
+ async function deviceError(res, operation) {
7345
8419
  const { SylphxError: SylphxError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
7346
8420
  const body = await res.text().catch(() => "");
7347
- let code = "platform_impersonation_error";
8421
+ let code = "device_flow_error";
7348
8422
  let message2 = `${operation} failed: HTTP ${res.status}`;
7349
8423
  try {
7350
8424
  const parsed = JSON.parse(body);
7351
8425
  if (parsed.error) code = parsed.error;
7352
8426
  if (parsed.error_description) message2 = parsed.error_description;
7353
- else if (parsed.message) message2 = parsed.message;
7354
8427
  } catch {
7355
8428
  }
7356
- let errorCode;
7357
- if (res.status === 401) errorCode = "UNAUTHORIZED";
7358
- else if (res.status === 403) errorCode = "UNAUTHORIZED";
7359
- else if (res.status === 404) errorCode = "NOT_FOUND";
7360
- else if (res.status === 409) errorCode = "CONFLICT";
7361
- else if (res.status === 429) errorCode = "TOO_MANY_REQUESTS";
7362
- else if (res.status >= 500) errorCode = "INTERNAL_SERVER_ERROR";
7363
- else errorCode = "BAD_REQUEST";
7364
8429
  return new SylphxError2(message2, {
7365
- code: errorCode,
8430
+ code: res.status === 429 ? "TOO_MANY_REQUESTS" : "BAD_REQUEST",
7366
8431
  status: res.status,
7367
8432
  data: { operation, code }
7368
8433
  });
@@ -10989,15 +12054,61 @@ export {
10989
12054
  ACHIEVEMENT_TIER_CONFIG,
10990
12055
  AuthenticationError,
10991
12056
  AuthorizationError,
12057
+ BILLING_ALLOWED_ROLES,
12058
+ BUILD_MINUTES_INCLUDED,
12059
+ BUILD_MINUTE_PRICES,
12060
+ BUILD_SIZE_MULTIPLIERS,
12061
+ BYTES_PER_GB,
12062
+ CI_BUILD_MINUTE_PRICE_MICRODOLLARS,
12063
+ CI_FREE_MINUTES_PER_MONTH,
12064
+ CI_MACOS_MULTIPLIER,
12065
+ CI_MACOS_SIZE_MULTIPLIERS,
12066
+ CI_SIZE_MULTIPLIERS,
12067
+ COMPUTE_PRICE_PER_HOUR_MICRODOLLARS,
12068
+ COMPUTE_RAM_RATE_MICRODOLLARS,
12069
+ COMPUTE_VCPU_ACTIVE_RATE_MICRODOLLARS,
12070
+ COMPUTE_VCPU_IDLE_RATE_MICRODOLLARS,
12071
+ CONSOLE_APP_SLUG,
12072
+ CREDENTIAL_REGEX,
12073
+ CREDIT_EXPIRY_MONTHS,
10992
12074
  CircuitBreakerOpenError,
12075
+ DEFAULT_MACHINE_SIZE,
12076
+ DEFAULT_MAX_REPLICAS,
12077
+ DEFAULT_POINTS_REWARD,
12078
+ DISCOUNT_DURATION_MONTHS,
12079
+ DISCOUNT_PERCENT,
10993
12080
  ERROR_CODE_STATUS,
12081
+ FREE_COMPUTE_HOURS,
12082
+ FREE_STORAGE_GB,
12083
+ HOURS_PER_MONTH,
12084
+ INSTANCE_TYPES,
12085
+ INSTANCE_TYPE_ALIASES,
12086
+ INSTANCE_TYPE_ORDER,
12087
+ INVOICE_DUE_DAYS,
10994
12088
  InvalidConnectionUrlError,
12089
+ KV_FREE_STORAGE_GB,
12090
+ LEGACY_INSTANCE_TYPE_ORDER,
12091
+ MACHINE_CONFIGS,
12092
+ MACHINE_MAX_INSTANCES,
12093
+ MACHINE_RESOURCE_REQUIREMENTS,
12094
+ MACHINE_SIZES,
12095
+ MAX_PASSWORD_LENGTH,
12096
+ MAX_PAYMENT_ATTEMPTS,
12097
+ MICRODOLLARS_PER_CENT,
12098
+ MIN_PASSWORD_LENGTH,
10995
12099
  NetworkError,
10996
12100
  NotFoundError,
12101
+ PASSWORD_REQUIREMENTS,
12102
+ PLATFORM_PLANS,
12103
+ PLATFORM_PLAN_ORDER,
12104
+ PLATFORM_PLAN_ORDER_ALL,
12105
+ PREMIUM_TRIAL_DAYS,
10997
12106
  RETRYABLE_CODES,
10998
12107
  RateLimitError,
10999
12108
  RunHandle,
11000
12109
  RunsClient,
12110
+ SERVICE_METRICS,
12111
+ STORAGE_PRICE_PER_GB_MONTH_MICRODOLLARS,
11001
12112
  SandboxClient,
11002
12113
  SandboxFiles,
11003
12114
  SandboxProcesses,
@@ -11005,6 +12116,7 @@ export {
11005
12116
  StepCompleteSignal,
11006
12117
  StepSleepSignal,
11007
12118
  SylphxError,
12119
+ TRANSFER_PRICE_PER_GB_MICRODOLLARS,
11008
12120
  TimeoutError,
11009
12121
  TriggersClient,
11010
12122
  ValidationError,
@@ -11016,6 +12128,8 @@ export {
11016
12128
  audit,
11017
12129
  authorizeOAuth,
11018
12130
  batchIndex,
12131
+ buildConnectionUrl,
12132
+ calculatePercentage,
11019
12133
  canDeleteOrganization,
11020
12134
  canManageMembers,
11021
12135
  canManageSettings,
@@ -11024,6 +12138,7 @@ export {
11024
12138
  captureException,
11025
12139
  captureExceptionRaw,
11026
12140
  captureMessage,
12141
+ centsToDollars,
11027
12142
  chat,
11028
12143
  chatStream,
11029
12144
  checkFlag,
@@ -11068,22 +12183,45 @@ export {
11068
12183
  dpop,
11069
12184
  embed,
11070
12185
  enableDebug,
12186
+ escapeCsvField,
12187
+ escapeHtml,
11071
12188
  exchangeOAuthCode,
11072
12189
  exponentialBackoff,
11073
12190
  exportUserData,
11074
12191
  extendedSignUp,
12192
+ getErrorDetails as extractErrorDetails,
12193
+ getErrorMessage as extractErrorMessage,
11075
12194
  forgotPassword,
12195
+ formatBytes,
12196
+ formatCents,
12197
+ formatCurrency,
12198
+ formatDate,
12199
+ formatDateTime,
12200
+ formatDuration,
12201
+ formatMicrodollars,
12202
+ formatMonthYear,
12203
+ formatNumber,
12204
+ formatPercent,
12205
+ formatRelativeTime,
12206
+ formatRelativeTimeShort,
12207
+ formatTime,
11076
12208
  functions,
11077
12209
  generateAnonymousId,
11078
12210
  generatePkce,
12211
+ generateReferralCode,
12212
+ generateSlug,
11079
12213
  getAchievement,
11080
12214
  getAchievementPoints,
11081
12215
  getAchievements,
12216
+ getActivePlans,
11082
12217
  getAllFlags,
11083
12218
  getAllSecrets,
11084
12219
  getAllStreaks,
12220
+ getAvailableInstanceTypes,
11085
12221
  getBackupCodes,
12222
+ getBaseUrl,
11086
12223
  getBillingBalance,
12224
+ getBillingStatusVariant,
11087
12225
  getBillingUsage,
11088
12226
  getBuildLogHistory,
11089
12227
  getCircuitBreakerState,
@@ -11092,13 +12230,17 @@ export {
11092
12230
  getDatabaseConnectionString,
11093
12231
  getDatabaseStatus,
11094
12232
  getDebugMode,
12233
+ getDefaultInstanceType,
11095
12234
  getDeployHistory,
11096
12235
  getDeployStatus,
11097
- getErrorCode,
11098
- getErrorMessage,
12236
+ getEnvPrefix,
12237
+ getErrorCode2 as getErrorCode,
12238
+ getErrorDetails2 as getErrorDetails,
12239
+ getErrorMessage2 as getErrorMessage,
11099
12240
  getFacets,
11100
12241
  getFlagPayload,
11101
12242
  getFlags,
12243
+ getInvoiceStatusVariant,
11102
12244
  getLeaderboard,
11103
12245
  getMemberPermissions,
11104
12246
  getMyReferralCode,
@@ -11108,6 +12250,7 @@ export {
11108
12250
  getOrganizationInvitations,
11109
12251
  getOrganizationMembers,
11110
12252
  getOrganizations,
12253
+ getPlanMonthlyPrice,
11111
12254
  getPlans,
11112
12255
  getProjectMetadata,
11113
12256
  getPromo,
@@ -11117,6 +12260,7 @@ export {
11117
12260
  getReferralStats,
11118
12261
  getRestErrorMessage,
11119
12262
  getRole,
12263
+ getSafeErrorMessage,
11120
12264
  getScheduledEmail,
11121
12265
  getScheduledEmailStats,
11122
12266
  getSearchStats,
@@ -11140,6 +12284,7 @@ export {
11140
12284
  getWebhookStats,
11141
12285
  hasAllPermissions,
11142
12286
  hasAnyPermission,
12287
+ hasBillingAccess,
11143
12288
  hasConsent,
11144
12289
  hasError,
11145
12290
  hasPermission,
@@ -11155,10 +12300,14 @@ export {
11155
12300
  introspectToken,
11156
12301
  inviteOrganizationMember,
11157
12302
  inviteUser,
12303
+ isChallengeRequired,
11158
12304
  isEmailConfigured,
11159
12305
  isEnabled,
12306
+ isMachineSize,
12307
+ isPlanDeprecated,
11160
12308
  isRetryableError,
11161
12309
  isSylphxError,
12310
+ isValidInstanceType,
11162
12311
  kvDelete,
11163
12312
  kvExists,
11164
12313
  kvExpire,
@@ -11196,9 +12345,13 @@ export {
11196
12345
  listUsers,
11197
12346
  markAllSecurityAlertsRead,
11198
12347
  markSecurityAlertRead,
12348
+ microsToDollars,
11199
12349
  oauth,
11200
12350
  page,
12351
+ parseConnectionUrl,
12352
+ parseMachineSize,
11201
12353
  parseOAuthCallback,
12354
+ parseUserAgent,
11202
12355
  password,
11203
12356
  pauseCron,
11204
12357
  platformAuth,
@@ -11229,12 +12382,20 @@ export {
11229
12382
  resetPassword,
11230
12383
  resetPlatformCookieCache,
11231
12384
  resetPlatformJwksCache,
12385
+ resolveCanonicalInstanceType,
12386
+ resolveMachineConfig,
12387
+ resolveMachineMaxInstances,
12388
+ resolveMachineResources,
12389
+ resolveMachineTierResources,
12390
+ resolveMaxReplicas,
12391
+ resolveResources,
11232
12392
  resumeCron,
11233
12393
  revokeAllTokens,
11234
12394
  revokeOrganizationInvitation,
11235
12395
  revokeToken,
11236
12396
  revokeUserSession,
11237
12397
  rollbackDeploy,
12398
+ safeJsonParse,
11238
12399
  scheduleEmail,
11239
12400
  scheduleTask,
11240
12401
  search,
@@ -11256,6 +12417,7 @@ export {
11256
12417
  submitScore,
11257
12418
  suspendUser,
11258
12419
  switchOrg,
12420
+ toPublicMachineSize,
11259
12421
  toSylphxError,
11260
12422
  track,
11261
12423
  trackBatch,
@@ -11275,6 +12437,7 @@ export {
11275
12437
  upsertDocument,
11276
12438
  user,
11277
12439
  userInfo,
12440
+ validateInstanceTypeForPlan,
11278
12441
  validatePromo,
11279
12442
  verifyAccessToken,
11280
12443
  verifyChallenge,