tands 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,10 @@
1
1
  // ../sdk/src/protocol.ts
2
2
  var isHome = (context) => Boolean(context.home);
3
+ function withReadOnly(context) {
4
+ const workspace = context.workspace;
5
+ if (!workspace || typeof workspace.readOnly === "boolean") return context;
6
+ return { ...context, workspace: { ...workspace, readOnly: false } };
7
+ }
3
8
 
4
9
  // ../sdk/src/server.ts
5
10
  var TokenError = class extends Error {
@@ -58,7 +63,7 @@ async function verifyToken(token, {
58
63
  throw new TokenError("It has expired.");
59
64
  }
60
65
  return {
61
- context: payload.ctx,
66
+ context: withReadOnly(payload.ctx),
62
67
  issuer: String(payload.iss),
63
68
  expiresAt: new Date(payload.exp * 1e3),
64
69
  action: typeof payload.act === "string" ? payload.act : null
@@ -43,6 +43,11 @@ function readAppMessage(data) {
43
43
  }
44
44
  return null;
45
45
  }
46
+ function withReadOnly(context) {
47
+ const workspace = context.workspace;
48
+ if (!workspace || typeof workspace.readOnly === "boolean") return context;
49
+ return { ...context, workspace: { ...workspace, readOnly: false } };
50
+ }
46
51
  function readHostMessage(data) {
47
52
  if (!isRecord(data) || data.tands !== PROTOCOL_VERSION) return null;
48
53
  const theme = data.theme === "dark" ? "dark" : "light";
@@ -51,7 +56,7 @@ function readHostMessage(data) {
51
56
  return isRecord(data.context) && typeof data.token === "string" && typeof data.expiresAt === "string" ? {
52
57
  tands: 1,
53
58
  type: "context",
54
- context: data.context,
59
+ context: withReadOnly(data.context),
55
60
  token: data.token,
56
61
  expiresAt: data.expiresAt,
57
62
  theme
@@ -70,6 +75,8 @@ function readHostMessage(data) {
70
75
  } : null;
71
76
  case "theme":
72
77
  return { tands: 1, type: "theme", theme };
78
+ case "readOnly":
79
+ return typeof data.readOnly === "boolean" ? { tands: 1, type: "readOnly", readOnly: data.readOnly } : null;
73
80
  default:
74
81
  return null;
75
82
  }
@@ -97,6 +104,7 @@ function connect({ timeoutMs = 1e4, window: win = window } = {}) {
97
104
  let theme = "light";
98
105
  let counter = 0;
99
106
  const themeListeners = /* @__PURE__ */ new Set();
107
+ const readOnlyListeners = /* @__PURE__ */ new Set();
100
108
  const waiting = /* @__PURE__ */ new Map();
101
109
  let pending = null;
102
110
  const send = (message) => parent.postMessage(message, hostOrigin ?? "*");
@@ -141,6 +149,11 @@ function connect({ timeoutMs = 1e4, window: win = window } = {}) {
141
149
  waiter.reject(new WorkspaceError(message.message, message.code ?? null));
142
150
  return;
143
151
  }
152
+ case "readOnly":
153
+ if (!context || context.workspace.readOnly === message.readOnly) return;
154
+ context = { ...context, workspace: { ...context.workspace, readOnly: message.readOnly } };
155
+ for (const listener of readOnlyListeners) listener(message.readOnly);
156
+ return;
144
157
  case "theme":
145
158
  theme = message.theme;
146
159
  for (const listener of themeListeners) listener(theme);
@@ -159,6 +172,9 @@ function connect({ timeoutMs = 1e4, window: win = window } = {}) {
159
172
  get context() {
160
173
  return context;
161
174
  },
175
+ get readOnly() {
176
+ return context.workspace.readOnly;
177
+ },
162
178
  get theme() {
163
179
  return theme;
164
180
  },
@@ -215,6 +231,10 @@ function connect({ timeoutMs = 1e4, window: win = window } = {}) {
215
231
  }
216
232
  };
217
233
  },
234
+ onReadOnly(listener) {
235
+ readOnlyListeners.add(listener);
236
+ return () => readOnlyListeners.delete(listener);
237
+ },
218
238
  onTheme(listener) {
219
239
  themeListeners.add(listener);
220
240
  return () => themeListeners.delete(listener);
package/dist/tands.js CHANGED
@@ -2580,8 +2580,8 @@ var Doc = class {
2580
2580
  const lines = content.split("\n").filter((x) => x);
2581
2581
  const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length));
2582
2582
  const dedented = lines.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x);
2583
- for (const line of dedented) {
2584
- this.content.push(line);
2583
+ for (const line2 of dedented) {
2584
+ this.content.push(line2);
2585
2585
  }
2586
2586
  }
2587
2587
  compile() {
@@ -19814,7 +19814,20 @@ var AppActionSchema = external_exports.object({
19814
19814
  /** It changes something (the default): agents ask first. `false`: it only reads. */
19815
19815
  changes: external_exports.boolean().default(true),
19816
19816
  /** What it's given, checked like a record's fields. */
19817
- input: external_exports.record(DataNameSchema, AppFieldSchema).refine((fields) => Object.keys(fields).length <= 20, "An action takes at most 20 inputs.").default({})
19817
+ input: external_exports.record(DataNameSchema, AppFieldSchema).refine((fields) => Object.keys(fields).length <= 20, "An action takes at most 20 inputs.").default({}),
19818
+ /**
19819
+ * Its view (decision 216): a page on the app's origin shown under the agent's
19820
+ * answer, told what the action was asked and answered (MCP Apps). Only for
19821
+ * actions that only read, for now.
19822
+ */
19823
+ view: external_exports.object({
19824
+ url: external_exports.string().min(1).max(500),
19825
+ /** What it's called above the page (default: the action's name). */
19826
+ title: external_exports.string().trim().min(1).max(60).optional()
19827
+ }).optional()
19828
+ }).refine((action) => !action.view || !action.changes, {
19829
+ message: "Only an action that only reads (changes: false) can have a view, for now.",
19830
+ path: ["view"]
19818
19831
  });
19819
19832
  var AppEventSchema = external_exports.object({
19820
19833
  /** What happened, for people choosing it in a trigger. */
@@ -19878,6 +19891,8 @@ var AppActionInfoSchema = external_exports.object({
19878
19891
  name: external_exports.string(),
19879
19892
  description: external_exports.string(),
19880
19893
  changes: external_exports.boolean(),
19894
+ /** It shows its own view under the agent's answer (decision 216). */
19895
+ view: external_exports.boolean().default(false),
19881
19896
  input: external_exports.array(
19882
19897
  external_exports.object({
19883
19898
  name: external_exports.string(),
@@ -19986,7 +20001,17 @@ var AppPreviewSchema = external_exports.object({
19986
20001
  var TabAppSchema = external_exports.object({ appId: external_exports.string(), tabId: external_exports.string() });
19987
20002
  var AppContextSchema = external_exports.object({
19988
20003
  user: external_exports.object({ id: external_exports.string(), name: external_exports.string() }),
19989
- workspace: external_exports.object({ id: external_exports.string(), slug: external_exports.string(), name: external_exports.string() }),
20004
+ /**
20005
+ * `readOnly` (decision 215): the workspace is read-only until its bill is
20006
+ * paid, so the app's writes are refused; its page and server can say so.
20007
+ * (False when a token from before it was told says nothing.)
20008
+ */
20009
+ workspace: external_exports.object({
20010
+ id: external_exports.string(),
20011
+ slug: external_exports.string(),
20012
+ name: external_exports.string(),
20013
+ readOnly: external_exports.boolean().default(false)
20014
+ }),
19990
20015
  /**
19991
20016
  * `name` is the one the reader sees: General and My Space in their language,
19992
20017
  * a DM by its other people. Not imported from ./space, which imports this file.
@@ -20896,6 +20921,126 @@ var EmailDraftSchema = external_exports.object({
20896
20921
  error: external_exports.string().nullable()
20897
20922
  });
20898
20923
 
20924
+ // ../types/src/cards.ts
20925
+ var MAX_MESSAGE_CARDS = 8;
20926
+ var line = (max) => external_exports.string().trim().min(1).max(max);
20927
+ var optionalLine = (max) => external_exports.string().trim().max(max).optional();
20928
+ var link = external_exports.url({ protocol: /^https?$/, error: "An http(s) address." }).max(2e3);
20929
+ var when = external_exports.union([
20930
+ external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "A date like 2030-01-08."),
20931
+ external_exports.iso.datetime({ offset: true })
20932
+ ]);
20933
+ var tone = external_exports.enum(["good", "warning", "bad", "neutral"]);
20934
+ var title = optionalLine(120);
20935
+ var EmailsCardSchema = external_exports.object({
20936
+ type: external_exports.literal("emails"),
20937
+ title,
20938
+ items: external_exports.array(
20939
+ external_exports.object({
20940
+ from: line(120),
20941
+ subject: external_exports.string().trim().max(200),
20942
+ snippet: optionalLine(300),
20943
+ at: when.optional(),
20944
+ url: link.optional(),
20945
+ unread: external_exports.boolean().optional(),
20946
+ important: external_exports.boolean().optional(),
20947
+ /** Why it matters, or what to do. */
20948
+ note: optionalLine(240)
20949
+ })
20950
+ ).min(1).max(20)
20951
+ });
20952
+ var EventsCardSchema = external_exports.object({
20953
+ type: external_exports.literal("events"),
20954
+ title,
20955
+ items: external_exports.array(
20956
+ external_exports.object({
20957
+ title: line(160),
20958
+ start: when,
20959
+ end: when.optional(),
20960
+ where: optionalLine(160),
20961
+ people: external_exports.array(line(80)).max(20).optional(),
20962
+ url: link.optional(),
20963
+ /** A video call's link. */
20964
+ join: link.optional(),
20965
+ /** The asker's answer to it. */
20966
+ status: external_exports.enum(["accepted", "tentative", "declined", "needs_answer"]).optional(),
20967
+ note: optionalLine(240)
20968
+ })
20969
+ ).min(1).max(20)
20970
+ });
20971
+ var MetricsCardSchema = external_exports.object({
20972
+ type: external_exports.literal("metrics"),
20973
+ title,
20974
+ items: external_exports.array(
20975
+ external_exports.object({
20976
+ label: line(60),
20977
+ value: line(40),
20978
+ detail: optionalLine(80),
20979
+ tone: tone.optional()
20980
+ })
20981
+ ).min(1).max(6)
20982
+ });
20983
+ var TableCardSchema = external_exports.object({
20984
+ type: external_exports.literal("table"),
20985
+ title,
20986
+ columns: external_exports.array(line(60)).min(1).max(8),
20987
+ rows: external_exports.array(external_exports.array(external_exports.union([external_exports.string().max(200), external_exports.number(), external_exports.null()])).max(8)).min(1).max(50)
20988
+ }).refine((card) => card.rows.every((row) => row.length === card.columns.length), {
20989
+ error: "Every row needs one cell per column.",
20990
+ path: ["rows"]
20991
+ });
20992
+ var ListCardSchema = external_exports.object({
20993
+ type: external_exports.literal("list"),
20994
+ title,
20995
+ items: external_exports.array(
20996
+ external_exports.object({
20997
+ title: line(160),
20998
+ detail: optionalLine(240),
20999
+ /** On the side: a date, an amount, a status. */
21000
+ meta: optionalLine(60),
21001
+ url: link.optional(),
21002
+ tone: tone.optional()
21003
+ })
21004
+ ).min(1).max(30)
21005
+ });
21006
+ var LinksCardSchema = external_exports.object({
21007
+ type: external_exports.literal("links"),
21008
+ title,
21009
+ items: external_exports.array(external_exports.object({ label: line(120), url: link, detail: optionalLine(160) })).min(1).max(12)
21010
+ });
21011
+ var NoteCardSchema = external_exports.object({
21012
+ type: external_exports.literal("note"),
21013
+ tone: external_exports.enum(["info", "success", "warning", "danger"]).default("info"),
21014
+ title,
21015
+ text: line(600)
21016
+ });
21017
+ var BarsCardSchema = external_exports.object({
21018
+ type: external_exports.literal("bars"),
21019
+ title,
21020
+ /** After each value: "h", "%", "€". */
21021
+ unit: optionalLine(12),
21022
+ items: external_exports.array(external_exports.object({ label: line(60), value: external_exports.number().finite().nonnegative() })).min(1).max(12)
21023
+ });
21024
+ var ActionsCardSchema = external_exports.object({
21025
+ type: external_exports.literal("actions"),
21026
+ items: external_exports.array(external_exports.object({ label: line(40), prompt: line(500) })).min(1).max(4)
21027
+ });
21028
+ var MessageCardSchema = external_exports.discriminatedUnion("type", [
21029
+ EmailsCardSchema,
21030
+ EventsCardSchema,
21031
+ MetricsCardSchema,
21032
+ TableCardSchema,
21033
+ ListCardSchema,
21034
+ LinksCardSchema,
21035
+ NoteCardSchema,
21036
+ BarsCardSchema,
21037
+ ActionsCardSchema
21038
+ ]);
21039
+ var MESSAGE_CARD_TYPES = MessageCardSchema.options.map(
21040
+ (option) => option.shape.type.value
21041
+ );
21042
+ var MessageCardsSchema = external_exports.array(MessageCardSchema).max(MAX_MESSAGE_CARDS);
21043
+
20899
21044
  // ../types/src/approvals.ts
20900
21045
  var ApprovalStatusSchema = external_exports.enum(["pending", "approved", "declined", "expired"]);
20901
21046
  var ApprovalSchema = external_exports.object({
@@ -20914,6 +21059,8 @@ var ApprovalSchema = external_exports.object({
20914
21059
  }).nullable(),
20915
21060
  /** The message, as proposed (or as approved, when edited). */
20916
21061
  body: external_exports.string(),
21062
+ /** Cards posted with it (decision 211: a recurring task's results). */
21063
+ cards: MessageCardsSchema.optional(),
20917
21064
  edited: external_exports.boolean(),
20918
21065
  /** Why, in the agent's words (optional); a workflow's step: what it asks. */
20919
21066
  reason: external_exports.string().nullable(),
@@ -21090,6 +21237,40 @@ var ChannelLoginSchema = external_exports.object({
21090
21237
  qr: external_exports.string().nullable()
21091
21238
  });
21092
21239
 
21240
+ // ../types/src/views.ts
21241
+ var MessageViewSchema = external_exports.object({
21242
+ id: external_exports.string().regex(/^[a-f0-9]{24}$/),
21243
+ /** Whose it is: a connector's (its page served by us, sandboxed), or an app's. */
21244
+ source: external_exports.object({ kind: external_exports.enum(["connector", "app"]), id: external_exports.string(), name: external_exports.string() }),
21245
+ /** The tool it shows (an app's: the action). */
21246
+ tool: external_exports.string(),
21247
+ /** An app's view: its page, on the app's origin (null: a connector's, served by us). */
21248
+ url: external_exports.string().nullable().default(null),
21249
+ title: external_exports.string().nullable(),
21250
+ /** Its server asks for a border (true), none (false), or leaves it to us (null). */
21251
+ border: external_exports.boolean().nullable()
21252
+ });
21253
+ var ViewToolResultSchema = external_exports.object({
21254
+ content: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())),
21255
+ structuredContent: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
21256
+ isError: external_exports.boolean().optional()
21257
+ });
21258
+ var MessageViewDataSchema = external_exports.object({
21259
+ tool: external_exports.object({
21260
+ name: external_exports.string(),
21261
+ title: external_exports.string().nullable(),
21262
+ description: external_exports.string(),
21263
+ inputSchema: external_exports.record(external_exports.string(), external_exports.unknown())
21264
+ }),
21265
+ input: external_exports.record(external_exports.string(), external_exports.unknown()),
21266
+ result: ViewToolResultSchema
21267
+ });
21268
+ var ViewCallRequestSchema = external_exports.object({
21269
+ name: external_exports.string().min(1).max(200),
21270
+ arguments: external_exports.record(external_exports.string(), external_exports.unknown()).default({})
21271
+ });
21272
+ var ViewCallResponseSchema = external_exports.object({ result: ViewToolResultSchema });
21273
+
21093
21274
  // ../types/src/message.ts
21094
21275
  var MAX_MESSAGE_LENGTH = 1e4;
21095
21276
  var MessageIdSchema = external_exports.string().regex(/^[a-f0-9]{24}$/, "Invalid message id");
@@ -21182,7 +21363,9 @@ var AgentToolSchema = external_exports.enum([
21182
21363
  // Connectors (M12 slice 2): a remote MCP server's tools the workspace chose (reading ones).
21183
21364
  "use_connector",
21184
21365
  // Notifications (decision 198): the asker's own Inbox, where only they read the answer.
21185
- "read_inbox"
21366
+ "read_inbox",
21367
+ // Cards (decision 211): mail, events, figures and rows shown under its answer.
21368
+ "show_cards"
21186
21369
  ]);
21187
21370
  var AgentStepSchema = external_exports.object({
21188
21371
  tool: AgentToolSchema,
@@ -21237,6 +21420,10 @@ var MessageSchema = external_exports.object({
21237
21420
  proposal: TaskProposalSchema.optional(),
21238
21421
  /** Something an agent wants to do, waiting for a person's yes (M8 slice 6). */
21239
21422
  approval: ApprovalSchema.optional(),
21423
+ /** What an agent's answer shows (decision 211): mail, events, figures, rows… */
21424
+ cards: MessageCardsSchema.optional(),
21425
+ /** Connectors' views in an agent's answer (MCP Apps, decision 213). */
21426
+ views: external_exports.array(MessageViewSchema).optional(),
21240
21427
  /** Sent from a messaging app (M13): Telegram… */
21241
21428
  via: MessageViaSchema.optional()
21242
21429
  });
@@ -21588,6 +21775,10 @@ var AUDIT_ACTIONS = {
21588
21775
  "workspace.data_moved",
21589
21776
  "workspace.previous_data_deleted",
21590
21777
  "workspace.support_plan_changed",
21778
+ "workspace.billing_contact_changed",
21779
+ "workspace.card_saved",
21780
+ "workspace.card_removed",
21781
+ "workspace.statement_paid",
21591
21782
  "workspace.space_imported",
21592
21783
  "audit.exported",
21593
21784
  "audit.destination_added",
@@ -21722,6 +21913,10 @@ var AUDIT_ACTIONS = {
21722
21913
  "billing.stripe_connected",
21723
21914
  "billing.stripe_disconnected",
21724
21915
  "statement.changed",
21916
+ "statement.extended",
21917
+ "agreement.created",
21918
+ "agreement.updated",
21919
+ "agreement.deleted",
21725
21920
  "support.registered",
21726
21921
  "support.unregistered",
21727
21922
  "support.report_sent",
@@ -21781,6 +21976,7 @@ var AuditTargetSchema = external_exports.object({
21781
21976
  "workflow",
21782
21977
  "approval",
21783
21978
  "statement",
21979
+ "agreement",
21784
21980
  "ticket",
21785
21981
  "help_request",
21786
21982
  "skill",
@@ -22220,6 +22416,297 @@ var ReadinessSchema = external_exports.object({
22220
22416
  checks: external_exports.array(ReadinessCheckSchema)
22221
22417
  });
22222
22418
 
22419
+ // ../types/src/billing.ts
22420
+ var COMMON_CURRENCIES = [
22421
+ "USD",
22422
+ "EUR",
22423
+ "GBP",
22424
+ "NGN",
22425
+ "KES",
22426
+ "GHS",
22427
+ "ZAR",
22428
+ "CAD",
22429
+ "AUD",
22430
+ "CHF",
22431
+ "JPY",
22432
+ "INR",
22433
+ "BRL"
22434
+ ];
22435
+ var CurrencySchema = external_exports.string().regex(/^[A-Z]{3}$/, "Use a three-letter currency code, like USD").refine((code) => knownCurrencies().has(code), "Unknown currency");
22436
+ var known = null;
22437
+ function knownCurrencies() {
22438
+ known ??= new Set(
22439
+ typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("currency") : COMMON_CURRENCIES
22440
+ );
22441
+ return known;
22442
+ }
22443
+ var PriceSchema = external_exports.number().min(0, "Prices can't be negative").max(1e6).transform((value) => Math.round(value * 1e4) / 1e4);
22444
+ var priced = external_exports.object({ enabled: external_exports.boolean(), price: PriceSchema });
22445
+ var ModelPriceSchema = external_exports.object({
22446
+ /** As agents name it: `provider/model`. */
22447
+ model: external_exports.string().trim().min(3).max(200),
22448
+ input: PriceSchema,
22449
+ /** Input read from the prompt cache. */
22450
+ cachedInput: PriceSchema,
22451
+ /** Input written to the prompt cache. */
22452
+ cacheWrite: PriceSchema,
22453
+ output: PriceSchema
22454
+ });
22455
+ var BillingSettingsSchema = external_exports.object({
22456
+ currency: CurrencySchema,
22457
+ /** Per person who used the workspace in the month. */
22458
+ seats: priced,
22459
+ /** Per GB-month in the deployment's bucket. */
22460
+ files: priced,
22461
+ /** Per GB-month in the deployment's MongoDB. */
22462
+ database: priced,
22463
+ /** Model cost (from `models`) plus a markup, for runs on the deployment's Gateway. */
22464
+ ai: external_exports.object({
22465
+ enabled: external_exports.boolean(),
22466
+ markupPercent: external_exports.number().min(0).max(1e3),
22467
+ models: external_exports.array(ModelPriceSchema).max(100).refine(
22468
+ (models) => new Set(models.map((m) => m.model)).size === models.length,
22469
+ "Each model once"
22470
+ )
22471
+ }),
22472
+ /** A flat amount per workspace per month (prorated from the day it was created). */
22473
+ deploymentFee: priced,
22474
+ /**
22475
+ * A support plan per workspace per month, opt-in (decision 74): workspaces
22476
+ * that turn it on pay it (prorated by day) and can ask the deployment for
22477
+ * help. Absent in settings saved before it existed: off.
22478
+ */
22479
+ support: priced.default({ enabled: false, price: 0 }),
22480
+ /**
22481
+ * The deployment's Gateways (decision 151), per hour: a workspace's own
22482
+ * ("Managed for you") while it's awake, or the deployment's shared one while
22483
+ * the workspace is connected to it. Absent in older settings: off.
22484
+ */
22485
+ gatewayHours: priced.default({ enabled: false, price: 0 }),
22486
+ /** A workspace's own Gateway's storage (its volume), per GB-month, asleep or awake. */
22487
+ gatewayStorage: priced.default({ enabled: false, price: 0 }),
22488
+ /**
22489
+ * What happens when a statement isn't paid (decision 209): reminders always;
22490
+ * with `readOnly`, the workspace becomes read-only `graceDays` after its
22491
+ * due date, until it's paid. Absent in older settings: on, 30 days.
22492
+ */
22493
+ overdue: external_exports.object({ readOnly: external_exports.boolean(), graceDays: external_exports.number().int().min(1).max(365) }).default({ readOnly: true, graceDays: 30 })
22494
+ });
22495
+ var BILLING_METERS = [
22496
+ "seats",
22497
+ "files",
22498
+ "database",
22499
+ "ai",
22500
+ "deploymentFee",
22501
+ "support",
22502
+ "gatewayHours",
22503
+ "gatewayStorage"
22504
+ ];
22505
+ var StripeConnectionSchema = external_exports.object({
22506
+ connected: external_exports.boolean(),
22507
+ /** Test keys create test invoices that nobody really pays. */
22508
+ mode: external_exports.enum(["test", "live"]).nullable(),
22509
+ /** The key's last four characters, to recognise it. */
22510
+ keyHint: external_exports.string().nullable(),
22511
+ connectedAt: external_exports.iso.datetime().nullable()
22512
+ });
22513
+ var ConnectStripeRequestSchema = external_exports.object({
22514
+ secretKey: external_exports.string().trim().regex(
22515
+ /^(sk|rk)_(test|live)_[A-Za-z0-9]{10,}$/,
22516
+ "Use a Stripe secret key (sk_\u2026) or restricted key (rk_\u2026)"
22517
+ )
22518
+ });
22519
+ var AdminBillingSchema = external_exports.object({
22520
+ settings: BillingSettingsSchema,
22521
+ /** Models agents have run on this deployment's Gateways, to price. */
22522
+ modelsInUse: external_exports.array(external_exports.string()),
22523
+ stripe: StripeConnectionSchema
22524
+ });
22525
+ var BillLineSchema = external_exports.object({
22526
+ meter: external_exports.enum([...BILLING_METERS, "agreement"]),
22527
+ /**
22528
+ * billed: charged; notBilled: this deployment doesn't bill it;
22529
+ * own: the workspace brings this resource itself; notChosen: an opt-in plan
22530
+ * (support) the workspace doesn't have; included: its agreement covers it.
22531
+ */
22532
+ status: external_exports.enum(["billed", "notBilled", "own", "notChosen", "included"]),
22533
+ /** People, GB-months, tokens, or the share of the month (deployment fee, support plan). */
22534
+ quantity: external_exports.number().nonnegative(),
22535
+ /** Price per unit; null for AI (priced per model). */
22536
+ unitPrice: external_exports.number().nullable(),
22537
+ amount: external_exports.number().nonnegative()
22538
+ });
22539
+ var WorkspaceBillSchema = external_exports.object({
22540
+ currency: external_exports.string(),
22541
+ lines: external_exports.array(BillLineSchema),
22542
+ /** AI amount per model (null: the model has no price set). */
22543
+ aiByModel: external_exports.array(external_exports.object({ model: external_exports.string(), amount: external_exports.number().nullable() })),
22544
+ /** Models used but not priced: their tokens aren't charged. */
22545
+ unpricedModels: external_exports.array(external_exports.string()),
22546
+ total: external_exports.number().nonnegative(),
22547
+ /** True for the current month: amounts grow until it ends. */
22548
+ toDate: external_exports.boolean()
22549
+ });
22550
+ var StatementStatusSchema = external_exports.enum(["nothingDue", "due", "paid", "void"]);
22551
+ var StatementSchema = external_exports.object({
22552
+ id: external_exports.string(),
22553
+ workspaceId: external_exports.string(),
22554
+ /**
22555
+ * usage: a month's metered bill, after it ends; agreement: a period of an
22556
+ * agreement's fixed price, at its start (decision 209).
22557
+ */
22558
+ kind: external_exports.enum(["usage", "agreement"]).default("usage"),
22559
+ /** usage: the month; agreement: the period's first month. */
22560
+ month: external_exports.string(),
22561
+ /** An agreement's period: its first and last months (null for usage). */
22562
+ period: external_exports.object({ from: external_exports.string(), to: external_exports.string() }).nullable().default(null),
22563
+ currency: external_exports.string(),
22564
+ lines: external_exports.array(BillLineSchema),
22565
+ aiByModel: external_exports.array(external_exports.object({ model: external_exports.string(), amount: external_exports.number().nullable() })),
22566
+ unpricedModels: external_exports.array(external_exports.string()),
22567
+ total: external_exports.number().nonnegative(),
22568
+ status: StatementStatusSchema,
22569
+ createdAt: external_exports.iso.datetime(),
22570
+ /** When it must be paid by (null: nothing due, or from before due dates were kept). */
22571
+ dueAt: external_exports.iso.datetime().nullable().default(null),
22572
+ paidAt: external_exports.iso.datetime().nullable(),
22573
+ payment: external_exports.object({
22574
+ /** stripe: an invoice on the deployment's Stripe; manual: settled outside the app. */
22575
+ provider: external_exports.enum(["stripe", "manual"]),
22576
+ invoiceNumber: external_exports.string().nullable(),
22577
+ /** Stripe's page where the invoice can be paid. */
22578
+ invoiceUrl: external_exports.url().nullable(),
22579
+ /** Why the invoice couldn't be created yet (retried every hour). */
22580
+ error: external_exports.string().nullable(),
22581
+ /** Why the saved card couldn't pay it (it can still be paid on Stripe's page). */
22582
+ chargeError: external_exports.string().nullable().default(null)
22583
+ })
22584
+ });
22585
+ var WorkspaceStatementsSchema = external_exports.object({ statements: external_exports.array(StatementSchema) });
22586
+ var AdminStatementsSchema = external_exports.object({
22587
+ month: external_exports.string(),
22588
+ /** Months with statements, newest first. */
22589
+ months: external_exports.array(external_exports.string()),
22590
+ statements: external_exports.array(
22591
+ StatementSchema.extend({
22592
+ workspace: external_exports.object({ id: external_exports.string(), name: external_exports.string(), slug: external_exports.string() })
22593
+ })
22594
+ )
22595
+ });
22596
+ var AgreementPeriodSchema = external_exports.enum(["month", "year"]);
22597
+ var AgreementSchema = external_exports.object({
22598
+ id: external_exports.string(),
22599
+ workspaceId: external_exports.string(),
22600
+ /** What it's called on invoices, like "Enterprise". */
22601
+ name: external_exports.string(),
22602
+ currency: external_exports.string(),
22603
+ /** The fixed price per period. */
22604
+ amount: external_exports.number().nonnegative(),
22605
+ period: AgreementPeriodSchema,
22606
+ startMonth: UsageMonthOf(),
22607
+ /** Its last month, included; null: until it's ended. */
22608
+ endMonth: UsageMonthOf().nullable(),
22609
+ included: external_exports.array(external_exports.enum(BILLING_METERS)),
22610
+ /** For the deployment's admins only (never shown to the workspace). */
22611
+ note: external_exports.string(),
22612
+ createdAt: external_exports.iso.datetime()
22613
+ });
22614
+ function UsageMonthOf() {
22615
+ return external_exports.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Use a month like 2026-11");
22616
+ }
22617
+ var agreementFields = {
22618
+ name: external_exports.string().trim().min(1, "Give it a name").max(80),
22619
+ amount: PriceSchema.refine((amount) => amount > 0, "Use an amount above 0"),
22620
+ period: AgreementPeriodSchema,
22621
+ startMonth: UsageMonthOf(),
22622
+ endMonth: UsageMonthOf().nullable(),
22623
+ included: external_exports.array(external_exports.enum(BILLING_METERS)).max(BILLING_METERS.length).transform((meters) => [...new Set(meters)]),
22624
+ note: external_exports.string().trim().max(2e3)
22625
+ };
22626
+ var endsAfterStart = (a) => !a.startMonth || !a.endMonth || a.endMonth >= a.startMonth;
22627
+ var CreateAgreementRequestSchema = external_exports.object({
22628
+ workspaceId: external_exports.string().min(1),
22629
+ ...agreementFields,
22630
+ note: agreementFields.note.default("")
22631
+ }).refine(endsAfterStart, { message: "It can't end before it starts", path: ["endMonth"] });
22632
+ var UpdateAgreementRequestSchema = external_exports.object(agreementFields).partial().refine((patch) => Object.keys(patch).length > 0, "Nothing to change.").refine(endsAfterStart, { message: "It can't end before it starts", path: ["endMonth"] });
22633
+ var AdminAgreementsSchema = external_exports.object({
22634
+ currency: external_exports.string(),
22635
+ /** The current month (UTC): agreements start then or later. */
22636
+ month: external_exports.string(),
22637
+ agreements: external_exports.array(
22638
+ AgreementSchema.extend({
22639
+ workspace: external_exports.object({ id: external_exports.string(), name: external_exports.string(), slug: external_exports.string() })
22640
+ })
22641
+ ),
22642
+ workspaces: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), slug: external_exports.string() }))
22643
+ });
22644
+ var BillingContactSchema = external_exports.object({
22645
+ /** The name on invoices: a company, or a person. */
22646
+ name: external_exports.string().trim().max(120),
22647
+ email: external_exports.union([external_exports.literal(""), external_exports.email()]),
22648
+ address: external_exports.object({
22649
+ line1: external_exports.string().trim().max(200),
22650
+ line2: external_exports.string().trim().max(200),
22651
+ city: external_exports.string().trim().max(100),
22652
+ postalCode: external_exports.string().trim().max(20),
22653
+ state: external_exports.string().trim().max(100),
22654
+ /** ISO 3166-1 alpha-2, or empty. */
22655
+ country: external_exports.union([
22656
+ external_exports.literal(""),
22657
+ external_exports.string().regex(/^[A-Z]{2}$/, "Use a two-letter country code")
22658
+ ])
22659
+ }),
22660
+ /** A VAT or tax number, printed on invoices. */
22661
+ taxId: external_exports.string().trim().max(50)
22662
+ });
22663
+ var SavedCardSchema = external_exports.object({
22664
+ brand: external_exports.string(),
22665
+ last4: external_exports.string(),
22666
+ expMonth: external_exports.number().int(),
22667
+ expYear: external_exports.number().int()
22668
+ });
22669
+ var BillingHoldSchema = external_exports.object({
22670
+ state: external_exports.enum(["overdue", "readOnly"]),
22671
+ /** The oldest unpaid statement's due date. */
22672
+ dueAt: external_exports.iso.datetime(),
22673
+ /** When it becomes read-only (null: the deployment doesn't do that). */
22674
+ readOnlyAt: external_exports.iso.datetime().nullable()
22675
+ });
22676
+ var WorkspaceBillingSchema = external_exports.object({
22677
+ /** null: the deployment doesn't bill workspaces. */
22678
+ plan: external_exports.discriminatedUnion("kind", [
22679
+ external_exports.object({
22680
+ kind: external_exports.literal("usage"),
22681
+ currency: external_exports.string(),
22682
+ /** The meters billed and their prices (null: priced per model, for AI). */
22683
+ meters: external_exports.array(
22684
+ external_exports.object({ meter: external_exports.enum(BILLING_METERS), unitPrice: external_exports.number().nullable() })
22685
+ )
22686
+ }),
22687
+ external_exports.object({
22688
+ kind: external_exports.literal("agreement"),
22689
+ currency: external_exports.string(),
22690
+ agreement: AgreementSchema.omit({ note: true }),
22691
+ /** The meters it doesn't include, billed as usual. */
22692
+ meters: external_exports.array(
22693
+ external_exports.object({ meter: external_exports.enum(BILLING_METERS), unitPrice: external_exports.number().nullable() })
22694
+ )
22695
+ })
22696
+ ]).nullable(),
22697
+ /** An agreement that starts later, if one is set. */
22698
+ upcoming: AgreementSchema.omit({ note: true }).nullable(),
22699
+ statements: external_exports.array(StatementSchema),
22700
+ contact: BillingContactSchema,
22701
+ /** Whether invoices can be paid online (the deployment has Stripe). */
22702
+ online: external_exports.boolean(),
22703
+ card: SavedCardSchema.nullable(),
22704
+ hold: BillingHoldSchema.nullable(),
22705
+ /** Owners change the contact and the card; admins see. */
22706
+ canManage: external_exports.boolean()
22707
+ });
22708
+ var CardSetupSchema = external_exports.object({ url: external_exports.url() });
22709
+
22223
22710
  // ../types/src/accent.ts
22224
22711
  var HexColorSchema = external_exports.string().regex(/^#[0-9a-fA-F]{6}$/, "Use a colour like #4f46e5").transform((hex3) => hex3.toLowerCase());
22225
22712
  var WorkspaceAccentSchema = external_exports.object({ light: HexColorSchema, dark: HexColorSchema });
@@ -22242,6 +22729,11 @@ var WorkspaceSchema = external_exports.object({
22242
22729
  logoUrl: external_exports.string().nullable().default(null),
22243
22730
  /** Its data is moving to new storage (M5 slice 4): read-only until it's done. */
22244
22731
  moving: external_exports.boolean().default(false),
22732
+ /**
22733
+ * Its bills are late (decision 209): owners and admins see it as overdue;
22734
+ * everyone sees it once it's read-only until paid. Null: nothing late.
22735
+ */
22736
+ billingHold: BillingHoldSchema.nullable().default(null),
22245
22737
  /** Its accent colour for each mode; null: the default ink. */
22246
22738
  accent: WorkspaceAccentSchema.nullable().default(null),
22247
22739
  /** Its default time zone (M8 slice 1): for schedules when a person's own isn't known. */
@@ -22750,166 +23242,6 @@ var SpaceImportResultSchema = external_exports.object({
22750
23242
  files: external_exports.number().int()
22751
23243
  });
22752
23244
 
22753
- // ../types/src/billing.ts
22754
- var COMMON_CURRENCIES = [
22755
- "USD",
22756
- "EUR",
22757
- "GBP",
22758
- "NGN",
22759
- "KES",
22760
- "GHS",
22761
- "ZAR",
22762
- "CAD",
22763
- "AUD",
22764
- "CHF",
22765
- "JPY",
22766
- "INR",
22767
- "BRL"
22768
- ];
22769
- var CurrencySchema = external_exports.string().regex(/^[A-Z]{3}$/, "Use a three-letter currency code, like USD").refine((code) => knownCurrencies().has(code), "Unknown currency");
22770
- var known = null;
22771
- function knownCurrencies() {
22772
- known ??= new Set(
22773
- typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("currency") : COMMON_CURRENCIES
22774
- );
22775
- return known;
22776
- }
22777
- var PriceSchema = external_exports.number().min(0, "Prices can't be negative").max(1e6).transform((value) => Math.round(value * 1e4) / 1e4);
22778
- var priced = external_exports.object({ enabled: external_exports.boolean(), price: PriceSchema });
22779
- var ModelPriceSchema = external_exports.object({
22780
- /** As agents name it: `provider/model`. */
22781
- model: external_exports.string().trim().min(3).max(200),
22782
- input: PriceSchema,
22783
- /** Input read from the prompt cache. */
22784
- cachedInput: PriceSchema,
22785
- /** Input written to the prompt cache. */
22786
- cacheWrite: PriceSchema,
22787
- output: PriceSchema
22788
- });
22789
- var BillingSettingsSchema = external_exports.object({
22790
- currency: CurrencySchema,
22791
- /** Per person who used the workspace in the month. */
22792
- seats: priced,
22793
- /** Per GB-month in the deployment's bucket. */
22794
- files: priced,
22795
- /** Per GB-month in the deployment's MongoDB. */
22796
- database: priced,
22797
- /** Model cost (from `models`) plus a markup, for runs on the deployment's Gateway. */
22798
- ai: external_exports.object({
22799
- enabled: external_exports.boolean(),
22800
- markupPercent: external_exports.number().min(0).max(1e3),
22801
- models: external_exports.array(ModelPriceSchema).max(100).refine(
22802
- (models) => new Set(models.map((m) => m.model)).size === models.length,
22803
- "Each model once"
22804
- )
22805
- }),
22806
- /** A flat amount per workspace per month (prorated from the day it was created). */
22807
- deploymentFee: priced,
22808
- /**
22809
- * A support plan per workspace per month, opt-in (decision 74): workspaces
22810
- * that turn it on pay it (prorated by day) and can ask the deployment for
22811
- * help. Absent in settings saved before it existed: off.
22812
- */
22813
- support: priced.default({ enabled: false, price: 0 }),
22814
- /**
22815
- * The deployment's Gateways (decision 151), per hour: a workspace's own
22816
- * ("Managed for you") while it's awake, or the deployment's shared one while
22817
- * the workspace is connected to it. Absent in older settings: off.
22818
- */
22819
- gatewayHours: priced.default({ enabled: false, price: 0 }),
22820
- /** A workspace's own Gateway's storage (its volume), per GB-month, asleep or awake. */
22821
- gatewayStorage: priced.default({ enabled: false, price: 0 })
22822
- });
22823
- var BILLING_METERS = [
22824
- "seats",
22825
- "files",
22826
- "database",
22827
- "ai",
22828
- "deploymentFee",
22829
- "support",
22830
- "gatewayHours",
22831
- "gatewayStorage"
22832
- ];
22833
- var StripeConnectionSchema = external_exports.object({
22834
- connected: external_exports.boolean(),
22835
- /** Test keys create test invoices that nobody really pays. */
22836
- mode: external_exports.enum(["test", "live"]).nullable(),
22837
- /** The key's last four characters, to recognise it. */
22838
- keyHint: external_exports.string().nullable(),
22839
- connectedAt: external_exports.iso.datetime().nullable()
22840
- });
22841
- var ConnectStripeRequestSchema = external_exports.object({
22842
- secretKey: external_exports.string().trim().regex(
22843
- /^(sk|rk)_(test|live)_[A-Za-z0-9]{10,}$/,
22844
- "Use a Stripe secret key (sk_\u2026) or restricted key (rk_\u2026)"
22845
- )
22846
- });
22847
- var AdminBillingSchema = external_exports.object({
22848
- settings: BillingSettingsSchema,
22849
- /** Models agents have run on this deployment's Gateways, to price. */
22850
- modelsInUse: external_exports.array(external_exports.string()),
22851
- stripe: StripeConnectionSchema
22852
- });
22853
- var BillLineSchema = external_exports.object({
22854
- meter: external_exports.enum(BILLING_METERS),
22855
- /**
22856
- * billed: charged; notBilled: this deployment doesn't bill it;
22857
- * own: the workspace brings this resource itself; notChosen: an opt-in plan
22858
- * (support) the workspace doesn't have.
22859
- */
22860
- status: external_exports.enum(["billed", "notBilled", "own", "notChosen"]),
22861
- /** People, GB-months, tokens, or the share of the month (deployment fee, support plan). */
22862
- quantity: external_exports.number().nonnegative(),
22863
- /** Price per unit; null for AI (priced per model). */
22864
- unitPrice: external_exports.number().nullable(),
22865
- amount: external_exports.number().nonnegative()
22866
- });
22867
- var WorkspaceBillSchema = external_exports.object({
22868
- currency: external_exports.string(),
22869
- lines: external_exports.array(BillLineSchema),
22870
- /** AI amount per model (null: the model has no price set). */
22871
- aiByModel: external_exports.array(external_exports.object({ model: external_exports.string(), amount: external_exports.number().nullable() })),
22872
- /** Models used but not priced: their tokens aren't charged. */
22873
- unpricedModels: external_exports.array(external_exports.string()),
22874
- total: external_exports.number().nonnegative(),
22875
- /** True for the current month: amounts grow until it ends. */
22876
- toDate: external_exports.boolean()
22877
- });
22878
- var StatementStatusSchema = external_exports.enum(["nothingDue", "due", "paid", "void"]);
22879
- var StatementSchema = external_exports.object({
22880
- id: external_exports.string(),
22881
- workspaceId: external_exports.string(),
22882
- month: external_exports.string(),
22883
- currency: external_exports.string(),
22884
- lines: external_exports.array(BillLineSchema),
22885
- aiByModel: external_exports.array(external_exports.object({ model: external_exports.string(), amount: external_exports.number().nullable() })),
22886
- unpricedModels: external_exports.array(external_exports.string()),
22887
- total: external_exports.number().nonnegative(),
22888
- status: StatementStatusSchema,
22889
- createdAt: external_exports.iso.datetime(),
22890
- paidAt: external_exports.iso.datetime().nullable(),
22891
- payment: external_exports.object({
22892
- /** stripe: an invoice on the deployment's Stripe; manual: settled outside the app. */
22893
- provider: external_exports.enum(["stripe", "manual"]),
22894
- invoiceNumber: external_exports.string().nullable(),
22895
- /** Stripe's page where the invoice can be paid. */
22896
- invoiceUrl: external_exports.url().nullable(),
22897
- /** Why the invoice couldn't be created yet (retried every hour). */
22898
- error: external_exports.string().nullable()
22899
- })
22900
- });
22901
- var WorkspaceStatementsSchema = external_exports.object({ statements: external_exports.array(StatementSchema) });
22902
- var AdminStatementsSchema = external_exports.object({
22903
- month: external_exports.string(),
22904
- /** Months with statements, newest first. */
22905
- months: external_exports.array(external_exports.string()),
22906
- statements: external_exports.array(
22907
- StatementSchema.extend({
22908
- workspace: external_exports.object({ id: external_exports.string(), name: external_exports.string(), slug: external_exports.string() })
22909
- })
22910
- )
22911
- });
22912
-
22913
23245
  // ../types/src/usage.ts
22914
23246
  var UsageMonthSchema = external_exports.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Use YYYY-MM");
22915
23247
  var UsageSourceSchema = external_exports.enum(["deployment", "own"]);
@@ -23297,7 +23629,11 @@ var ConnectorToolSchema = external_exports.object({
23297
23629
  /** A tool its server doesn't describe, which an owner says only reads. */
23298
23630
  markedReadOnly: external_exports.boolean(),
23299
23631
  /** Agents may use it (only tools that read, or that an owner says only read). */
23300
- enabled: external_exports.boolean()
23632
+ enabled: external_exports.boolean(),
23633
+ /** It shows a view in conversations (MCP Apps, decision 213). */
23634
+ view: external_exports.boolean().default(false),
23635
+ /** Only its views call it, not agents. */
23636
+ viewsOnly: external_exports.boolean().default(false)
23301
23637
  });
23302
23638
  var McpConnectorSchema = external_exports.object({
23303
23639
  id: external_exports.string(),
@@ -24000,6 +24336,7 @@ function checkManifest(json2, url2) {
24000
24336
  manifest2.tabs.forEach((tab) => onOrigin(tab.url, `tabs.${tab.id}.url`));
24001
24337
  for (const [name, action] of Object.entries(manifest2.actions ?? {})) {
24002
24338
  onOrigin(action.url, `actions.${name}.url`);
24339
+ if (action.view) onOrigin(action.view.url, `actions.${name}.view.url`);
24003
24340
  }
24004
24341
  if (manifest2.icon) onOrigin(manifest2.icon, "icon");
24005
24342
  }
@@ -24075,6 +24412,7 @@ function harnessPage() {
24075
24412
  input, select, textarea { width: 100%; padding: 7px 9px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); }
24076
24413
  button { padding: 7px 12px; border-radius: 8px; border: 1px solid var(--border); background: var(--canvas); cursor: pointer; }
24077
24414
  button.primary { background: var(--accent); color: var(--canvas); border-color: var(--accent); }
24415
+ button[aria-pressed="true"] { background: var(--text); color: var(--canvas); border-color: var(--text); }
24078
24416
  .frame { display: flex; flex-direction: column; }
24079
24417
  .frame .bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border); color: var(--muted); font-size: 12px; }
24080
24418
  .frame .bar code { color: var(--text); }
@@ -24098,6 +24436,7 @@ function harnessPage() {
24098
24436
  <strong>App harness</strong><span class="muted" id="appName"></span>
24099
24437
  <span class="spacer"></span>
24100
24438
  <button type="button" id="theme">Dark</button>
24439
+ <button type="button" id="readOnly" aria-pressed="false" title="As when its bill is unpaid: reads work, writes are refused">Read-only</button>
24101
24440
  <button type="button" id="reset">Clear data</button>
24102
24441
  </header>
24103
24442
  <div id="problems" class="problems" hidden></div>
@@ -24141,6 +24480,8 @@ function harnessPage() {
24141
24480
  const $ = (id) => document.getElementById(id);
24142
24481
  const frame = $("frame");
24143
24482
  let theme = "light";
24483
+ // The workspace read-only (an unpaid bill): the page is told, and writes are refused.
24484
+ let readOnly = false;
24144
24485
  let info = null;
24145
24486
  let origin = null;
24146
24487
 
@@ -24148,7 +24489,7 @@ const post = async (path, body) => {
24148
24489
  const response = await fetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
24149
24490
  return response.json();
24150
24491
  };
24151
- const who = () => Object.fromEntries(new FormData($("who")));
24492
+ const who = () => ({ ...Object.fromEntries(new FormData($("who"))), readOnly });
24152
24493
  const send = (message) => frame.contentWindow?.postMessage({ tands: 1, ...message }, origin);
24153
24494
  const reply = (id, out) =>
24154
24495
  out && out.error ? send({ type: "error", id, code: out.error.code, message: out.error.message }) : send({ type: "data", id, result: out.result });
@@ -24251,6 +24592,11 @@ $("theme").addEventListener("click", () => {
24251
24592
  $("theme").textContent = theme === "light" ? "Dark" : "Light";
24252
24593
  send({ type: "theme", theme });
24253
24594
  });
24595
+ $("readOnly").addEventListener("click", () => {
24596
+ readOnly = !readOnly;
24597
+ $("readOnly").setAttribute("aria-pressed", String(readOnly));
24598
+ send({ type: "readOnly", readOnly });
24599
+ });
24254
24600
  $("reset").addEventListener("click", async () => { await post("/harness/reset", {}); void refresh(); });
24255
24601
  void load().then(refresh);
24256
24602
  setInterval(refresh, 2000);
@@ -24420,6 +24766,9 @@ function createMemoryStore() {
24420
24766
  }
24421
24767
 
24422
24768
  // src/harness.ts
24769
+ var READING = /* @__PURE__ */ new Set(["values.get", "values.list", "records.list", "records.get"]);
24770
+ var READ_ONLY_MESSAGE = "This workspace is read-only until its bill is paid.";
24771
+ var writes = (request) => !READING.has(String(request?.op));
24423
24772
  var ACTION_TOKEN_SECONDS = 120;
24424
24773
  var DATA_KEY_MS = 5 * 6e4;
24425
24774
  var ACTION_TIMEOUT_MS = 1e4;
@@ -24468,7 +24817,12 @@ async function startHarness({
24468
24817
  const spaceId = idFrom("s", who.spaceName || "Design");
24469
24818
  return {
24470
24819
  user: { id: idFrom("u", who.name || "Ada Lovelace"), name: who.name || "Ada Lovelace" },
24471
- workspace: { id: "dev-workspace", slug: "dev", name: "Harness workspace" },
24820
+ workspace: {
24821
+ id: "dev-workspace",
24822
+ slug: "dev",
24823
+ name: "Harness workspace",
24824
+ readOnly: who.readOnly === true
24825
+ },
24472
24826
  space: { id: spaceId, kind: who.spaceKind, name: who.spaceName || "Design" },
24473
24827
  tab: { id: `${spaceId}-${who.appTabId}`, appTabId: who.appTabId },
24474
24828
  app: { id: manifest2.id },
@@ -24503,7 +24857,7 @@ async function startHarness({
24503
24857
  }
24504
24858
  const isWho = (value) => {
24505
24859
  const who = value;
24506
- return typeof who?.name === "string" && typeof who.spaceName === "string" && typeof who.locale === "string" && typeof who.appTabId === "string" && ["normal", "general", "personal", "dm"].includes(String(who.spaceKind));
24860
+ return typeof who?.name === "string" && typeof who.spaceName === "string" && typeof who.locale === "string" && typeof who.appTabId === "string" && (who.readOnly === void 0 || typeof who.readOnly === "boolean") && ["normal", "general", "personal", "dm"].includes(String(who.spaceKind));
24507
24861
  };
24508
24862
  function logEvent(loaded, body, from, person) {
24509
24863
  const manifest2 = loaded.manifest;
@@ -24633,6 +24987,9 @@ async function startHarness({
24633
24987
  }
24634
24988
  const loaded2 = await current();
24635
24989
  if (!loaded2.manifest) return refused2(409, "BAD_MANIFEST", loaded2.problems.join(" "));
24990
+ if (claims.who.readOnly && (url2.pathname === "/api/apps/events" || writes(body))) {
24991
+ return refused2(423, "WORKSPACE_READ_ONLY", READ_ONLY_MESSAGE);
24992
+ }
24636
24993
  if (url2.pathname === "/api/apps/events") {
24637
24994
  const outcome2 = logEvent(loaded2, body, "server", claims.who.name);
24638
24995
  return "error" in outcome2 ? send(outcome2.status, { error: outcome2.error }) : send(200, { result: outcome2.result });
@@ -24649,6 +25006,9 @@ async function startHarness({
24649
25006
  return refused2(404, "NOT_FOUND", `${manifest2.name} has no tab \u201C${who.appTabId}\u201D.`);
24650
25007
  }
24651
25008
  const context = contextFor(who, manifest2);
25009
+ if (who.readOnly && (url2.pathname === "/harness/event" || url2.pathname === "/harness/data" && writes(body.request))) {
25010
+ return refused2(423, "WORKSPACE_READ_ONLY", READ_ONLY_MESSAGE);
25011
+ }
24652
25012
  switch (url2.pathname) {
24653
25013
  case "/harness/token": {
24654
25014
  const tab = manifest2.tabs.find((one) => one.id === who.appTabId);
@@ -25001,7 +25361,7 @@ your manifest's address. See https://developer.tabsandspaces.dev.
25001
25361
  `;
25002
25362
 
25003
25363
  // src/bin.ts
25004
- var version2 = true ? "0.2.0" : "dev";
25364
+ var version2 = true ? "0.3.0" : "dev";
25005
25365
  var USAGE = `tands: developer tools for Tabs & Spaces apps
25006
25366
 
25007
25367
  tands new <folder> [--name "My app"] [--id my-app]
@@ -25027,8 +25387,8 @@ var USAGE = `tands: developer tools for Tabs & Spaces apps
25027
25387
  Docs: https://developer.tabsandspaces.dev`;
25028
25388
  async function main(argv, {
25029
25389
  cwd = process.env.INIT_CWD ?? process.cwd(),
25030
- out = (line) => console.log(line),
25031
- err = (line) => console.error(line),
25390
+ out = (line2) => console.log(line2),
25391
+ err = (line2) => console.error(line2),
25032
25392
  env = process.env,
25033
25393
  fetchImpl = fetch
25034
25394
  } = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tands",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Developer tools for Tabs & Spaces apps: `tands new`, `tands check`, `tands dev` (a local harness that stands in for a workspace) and `tands publish`.",
5
5
  "license": "MIT",
6
6
  "author": "Tabs & Spaces",
@@ -38,7 +38,7 @@
38
38
  "test:coverage": "vitest run --coverage"
39
39
  },
40
40
  "devDependencies": {
41
- "@tabs-and-spaces/sdk": "0.3.0",
41
+ "@tabs-and-spaces/sdk": "0.5.0",
42
42
  "@tabs-and-spaces/types": "0.0.0",
43
43
  "@types/node": "^26.6.3",
44
44
  "@vitest/coverage-v8": "5.0.2",