mockpit 1.2.0 → 2.0.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.
Files changed (59) hide show
  1. package/README.md +69 -48
  2. package/bin/demoData.js +3 -3
  3. package/bin/mockpit.js +426 -51
  4. package/dist/mcp/server.js +45 -7
  5. package/dist/server/apiViews.d.ts +407 -21
  6. package/dist/server/apiViews.js +110 -11
  7. package/dist/server/app.d.ts +5 -1
  8. package/dist/server/app.js +1454 -192
  9. package/dist/server/codeWorker.js +2 -1
  10. package/dist/server/decisions.d.ts +23 -0
  11. package/dist/server/decisions.js +100 -0
  12. package/dist/server/designGuide.d.ts +6 -2
  13. package/dist/server/designGuide.js +96 -27
  14. package/dist/server/embeds.d.ts +26 -0
  15. package/dist/server/embeds.js +144 -0
  16. package/dist/server/eventLog.js +2 -1
  17. package/dist/server/feedbackBatch.d.ts +15 -4
  18. package/dist/server/feedbackBatch.js +31 -7
  19. package/dist/server/fills.d.ts +41 -0
  20. package/dist/server/fills.js +328 -0
  21. package/dist/server/guidance.d.ts +23 -0
  22. package/dist/server/guidance.js +71 -0
  23. package/dist/server/index.js +25 -2
  24. package/dist/server/knobs.d.ts +6 -2
  25. package/dist/server/knobs.js +11 -1
  26. package/dist/server/mcpHttp.d.ts +6 -0
  27. package/dist/server/mcpHttp.js +35 -0
  28. package/dist/server/mcpSpec.d.ts +14 -13
  29. package/dist/server/mcpSpec.js +133 -18
  30. package/dist/server/postSurfaces.js +6 -12
  31. package/dist/server/richRender.js +3 -1
  32. package/dist/server/run.js +10 -1
  33. package/dist/server/runApi.d.ts +3 -3
  34. package/dist/server/runApi.js +18 -13
  35. package/dist/server/sqlStore.d.ts +97 -5
  36. package/dist/server/sqlStore.js +367 -27
  37. package/dist/server/surfacePage.d.ts +6 -2
  38. package/dist/server/surfacePage.js +142 -10
  39. package/dist/server/themes.d.ts +2 -0
  40. package/dist/server/themes.js +23 -3
  41. package/dist/server/types.d.ts +107 -16
  42. package/dist/server/types.js +73 -10
  43. package/dist/server/viewerRequest.d.ts +2 -0
  44. package/dist/server/viewerRequest.js +14 -0
  45. package/extensions/mockpit.js +179 -16
  46. package/guide/packs/design-craft.md +35 -0
  47. package/guide/topics/asks.md +82 -31
  48. package/guide/topics/feedback.md +151 -43
  49. package/guide/topics/html.md +8 -10
  50. package/guide/topics/http.md +8 -4
  51. package/guide/topics/knobs.md +4 -3
  52. package/guide/topics/scripts.md +2 -2
  53. package/guide/topics/surfaces.md +45 -11
  54. package/package.json +3 -15
  55. package/skills/launch-video/SKILL.md +2 -2
  56. package/skills/mockpit/SKILL.md +23 -7
  57. package/viewer/dist/index.html +9 -8
  58. package/dist/server/slots.d.ts +0 -14
  59. package/dist/server/slots.js +0 -71
@@ -68,6 +68,7 @@ export interface HtmlSurface {
68
68
  kind: "html";
69
69
  html: string;
70
70
  kits?: string[];
71
+ poster?: string;
71
72
  }
72
73
  export interface MarkdownSurface {
73
74
  kind: "markdown";
@@ -196,15 +197,27 @@ export interface PadValue {
196
197
  y: number;
197
198
  }
198
199
  export type KnobValue = number | boolean | string | PadValue | SpringKnob | EasingKnob;
199
- export type MockKind = "page" | "component";
200
+ export type MockKind = "page" | "component" | "sketch";
201
+ export declare const isMockKind: (k: unknown) => k is MockKind;
200
202
  export type PostStatus = "open" | "accepted" | "archived";
203
+ export type VariantStatus = "open" | "inUse" | "archived";
204
+ export declare const variantStatus: (status: PostStatus) => VariantStatus;
205
+ export declare const isLive: (status: PostStatus | VariantStatus) => boolean;
206
+ export declare const inUseVersion: (post: Pick<Post, "status" | "version" | "acceptedVersion">) => number;
207
+ export declare function postAtVersion(post: Post, version: number): Post | null;
208
+ export declare function versionAt(post: Pick<Post, "version" | "history">, n: number): number;
201
209
  export interface AskOption {
202
210
  id: string;
203
211
  label: string;
204
212
  variant?: string;
213
+ version?: number;
205
214
  set?: Record<string, KnobValue>;
215
+ recommended?: true;
206
216
  }
207
217
  export type AskAnswer = string | string[];
218
+ export declare const optionState: (ask: Pick<Ask, "state">, mock: {
219
+ states: string[];
220
+ }) => string | null;
208
221
  export interface Ask {
209
222
  id: string;
210
223
  text: string;
@@ -213,11 +226,13 @@ export interface Ask {
213
226
  part?: string;
214
227
  options: AskOption[];
215
228
  multi?: boolean;
229
+ after?: string;
216
230
  answer?: AskAnswer;
217
231
  other?: string;
218
232
  note?: string;
219
233
  otherImages?: string[];
220
234
  noteImages?: string[];
235
+ dismissed?: true;
221
236
  at: string;
222
237
  }
223
238
  export declare const OTHER_ID = "other";
@@ -237,16 +252,22 @@ export interface PartComment {
237
252
  anchor?: PartCommentAnchor;
238
253
  reaction?: Reaction;
239
254
  variant?: string;
255
+ version?: number;
256
+ borrow?: {
257
+ from: string;
258
+ part?: string;
259
+ };
240
260
  }
241
- export interface ReplyDecision {
242
- kind: "accept" | "revise" | "drop";
261
+ export interface ReplyVerdict {
262
+ kind: "use" | "revise";
243
263
  state: string | null;
244
264
  variant: string;
265
+ version?: number;
245
266
  }
246
267
  export interface Draft {
247
268
  version: number;
248
269
  answers: Record<string, AskAnswer>;
249
- mix: Record<string, string>;
270
+ borrowed: Record<string, string>;
250
271
  tuned: Record<string, KnobValue>;
251
272
  comments: PartComment[];
252
273
  others?: Record<string, string>;
@@ -259,7 +280,7 @@ export interface Reply {
259
280
  mockId: string;
260
281
  version: number;
261
282
  answers: Record<string, AskAnswer>;
262
- mix: Record<string, string>;
283
+ borrowed: Record<string, string>;
263
284
  tuned: Record<string, KnobValue>;
264
285
  comments: PartComment[];
265
286
  others?: Record<string, string>;
@@ -267,26 +288,45 @@ export interface Reply {
267
288
  otherImages?: Record<string, string[]>;
268
289
  noteImages?: Record<string, string[]>;
269
290
  text?: string;
270
- decision?: ReplyDecision;
291
+ verdict?: ReplyVerdict;
292
+ closed?: {
293
+ dismissed: string[];
294
+ };
295
+ edited?: MockEdit;
296
+ }
297
+ export interface MockEdit {
298
+ kind?: MockKind;
299
+ area?: string | null;
271
300
  }
301
+ export declare const AREA_ORDER_SETTING = "areaOrder";
272
302
  export interface Mock {
273
303
  id: string;
274
304
  project: string;
275
305
  slug: string;
276
306
  title: string;
277
307
  kind: MockKind;
308
+ area: string | null;
309
+ source: string | null;
278
310
  states: string[];
311
+ situations: Record<string, string[]>;
279
312
  asks: Ask[];
280
313
  knobs: Knobs;
314
+ widths: number[];
281
315
  draft: Draft | null;
282
316
  sessionId: string | null;
317
+ closedAt: string | null;
283
318
  createdAt: string;
284
319
  updatedAt: string;
285
320
  }
286
- export interface Slot {
321
+ export declare const NORMAL_SITUATION = "Normal";
322
+ export declare const situationKey: (state: string, situation: string) => string;
323
+ export declare function momentOf(situations: Record<string, string[]>, state: string | null): string | null;
324
+ export declare const momentsOf: (states: string[], situations: Record<string, string[]>) => string[];
325
+ export interface Embed {
287
326
  slug: string;
288
327
  variant: string;
289
328
  version: number;
329
+ pinned?: true;
290
330
  }
291
331
  export interface PostVersion {
292
332
  version: number;
@@ -297,6 +337,10 @@ export interface PostVersion {
297
337
  prompt?: string;
298
338
  author?: string;
299
339
  }
340
+ export interface ForkRef {
341
+ variant: string;
342
+ version: number;
343
+ }
300
344
  export interface Post {
301
345
  id: string;
302
346
  sessionId: string;
@@ -311,11 +355,28 @@ export interface Post {
311
355
  version: number;
312
356
  history: PostVersion[];
313
357
  knobs?: Knobs;
314
- slots: Slot[];
358
+ note?: string;
359
+ embeds: Embed[];
360
+ forkedFrom?: ForkRef;
361
+ starred?: true;
362
+ acceptedVersion?: number;
363
+ hiddenVersions?: number[];
315
364
  from?: number;
316
365
  prompt?: string;
317
366
  author?: string;
318
367
  }
368
+ export interface FlowStep {
369
+ mock: string;
370
+ state: string | null;
371
+ }
372
+ export interface Flow {
373
+ project: string;
374
+ name: string;
375
+ steps: FlowStep[];
376
+ createdAt: string;
377
+ updatedAt: string;
378
+ }
379
+ export declare const sameScreen: (a: FlowStep, b: FlowStep) => boolean;
319
380
  export interface ProjectSummary {
320
381
  name: string;
321
382
  mocks: number;
@@ -468,17 +529,25 @@ export interface CreateMockInput {
468
529
  slug: string;
469
530
  title?: string;
470
531
  kind?: MockKind;
532
+ area?: string | null;
533
+ source?: string | null;
471
534
  states?: string[];
472
535
  knobs?: Knobs;
536
+ widths?: number[];
473
537
  sessionId?: string | null;
474
538
  }
475
539
  export interface UpdateMockInput {
476
540
  title?: string;
477
541
  kind?: MockKind;
542
+ area?: string | null;
543
+ source?: string | null;
478
544
  states?: string[];
545
+ situations?: Record<string, string[]>;
479
546
  asks?: Ask[];
480
547
  knobs?: Knobs;
548
+ widths?: number[];
481
549
  sessionId?: string | null;
550
+ closedAt?: string | null;
482
551
  }
483
552
  export interface CreatePostInput {
484
553
  sessionId: string;
@@ -488,7 +557,9 @@ export interface CreatePostInput {
488
557
  title?: string;
489
558
  surfaces: Surface[];
490
559
  knobs?: Knobs;
491
- slots?: Slot[];
560
+ note?: string;
561
+ embeds?: Embed[];
562
+ forkedFrom?: ForkRef;
492
563
  from?: number;
493
564
  prompt?: string;
494
565
  author?: string;
@@ -497,7 +568,8 @@ export interface UpdatePostInput {
497
568
  title?: string;
498
569
  surfaces?: Surface[];
499
570
  knobs?: Knobs | null;
500
- slots?: Slot[];
571
+ note?: string;
572
+ embeds?: Embed[];
501
573
  from?: number;
502
574
  prompt?: string;
503
575
  author?: string;
@@ -528,8 +600,13 @@ export interface CommitReplyInput {
528
600
  text: string;
529
601
  payload: Reply;
530
602
  asks: Ask[];
531
- accept: string[];
603
+ accept: {
604
+ id: string;
605
+ version: number;
606
+ }[];
532
607
  archive: string[];
608
+ closedAt?: string;
609
+ keepDraft?: true;
533
610
  }
534
611
  export interface PostQuery {
535
612
  mockId?: string;
@@ -554,6 +631,10 @@ export interface Store {
554
631
  createMock(input: CreateMockInput): Promise<Mock>;
555
632
  updateMock(id: string, patch: UpdateMockInput): Promise<Mock | null>;
556
633
  removeMock(id: string): Promise<boolean>;
634
+ renameMock(id: string, slug: string): Promise<Mock | null>;
635
+ renameProject(from: string, to: string): Promise<boolean>;
636
+ listFlows(project: string): Promise<Flow[]>;
637
+ placeFlowStep(project: string, name: string, screen: FlowStep, step: number): Promise<Flow | null>;
557
638
  putDraft(mockId: string, draft: Draft | null): Promise<Mock | null>;
558
639
  listPosts(query?: PostQuery): Promise<Post[]>;
559
640
  countPostsBySession(): Promise<Map<string, number>>;
@@ -562,7 +643,9 @@ export interface Store {
562
643
  createPost(input: CreatePostInput): Promise<Post | null>;
563
644
  updatePost(id: string, patch: UpdatePostInput): Promise<Post | null>;
564
645
  removePost(id: string): Promise<boolean>;
565
- setPostStatus(id: string, status: PostStatus): Promise<Post | null>;
646
+ setPostStatus(id: string, status: PostStatus, acceptedVersion?: number): Promise<Post | null>;
647
+ setPostStarred(id: string, starred: boolean): Promise<Post | null>;
648
+ setVersionHidden(id: string, version: number, hidden: boolean): Promise<Post | null>;
566
649
  listComments(query: CommentQuery): Promise<Comment[]>;
567
650
  createComment(input: CreateCommentInput): Promise<Comment | null>;
568
651
  removeComment(id: string): Promise<Comment | null>;
@@ -600,10 +683,18 @@ export declare function projectFromCwd(cwd: string | null | undefined): string |
600
683
  export declare const htmlSurface: (html: string, kits?: unknown) => HtmlSurface;
601
684
  export declare const askAnswered: (a: Pick<Ask, "answer" | "other" | "note">) => boolean;
602
685
  export declare const openAsks: (mock: Pick<Mock, "asks">) => Ask[];
603
- export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
604
- export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
605
- export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
606
- export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
686
+ export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states"> & {
687
+ closedAt?: string | null;
688
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
689
+ export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states"> & {
690
+ closedAt?: string | null;
691
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
692
+ export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states"> & {
693
+ closedAt?: string | null;
694
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
695
+ export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft"> & {
696
+ closedAt?: string | null;
697
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
607
698
  export declare const BUILTIN_ASK_ID = "variant";
608
699
  export declare function builtinAsk(mock: Pick<Mock, "states">, posts: Pick<Post, "state" | "variant">[], id: string): Ask | undefined;
609
700
  export declare function summarizeProjects(mocks: Mock[], sessions: Session[], posts?: Post[]): ProjectSummary[];
@@ -9,9 +9,8 @@
9
9
  // build their `kind` enums from it, so a kind can't be added to the model
10
10
  // without the MCP tier advertising it too (the gap that left `json`/`code`
11
11
  // publishable over CLI/REST but invisible to MCP). The per-kind FIELD schemas
12
- // in postSurfaces.ts and mcpSpec.ts are still hand-written; test/mcpSpec.test.ts
13
- // guards that every kind here round-trips through both the MCP schema and the
14
- // validator with its fields, so neither half can silently fall behind.
12
+ // in postSurfaces.ts and mcpSpec.ts are still hand-written: a kind added here
13
+ // needs its fields in both, or one half silently falls behind.
15
14
  export const SURFACE_KINDS = [
16
15
  "html",
17
16
  "diff",
@@ -51,6 +50,44 @@ export function isSurfaceKind(kind) {
51
50
  export function isSandboxedSurfaceKind(kind) {
52
51
  return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
53
52
  }
53
+ export const isMockKind = (k) => k === "page" || k === "component" || k === "sketch";
54
+ export const variantStatus = (status) => status === "accepted" ? "inUse" : status;
55
+ // Still in the running: what the user can choose between.
56
+ export const isLive = (status) => status === "open" || status === "accepted" || status === "inUse";
57
+ // The version a variant is resolved at when nothing names one: the version in
58
+ // use when accepted, else its latest.
59
+ export const inUseVersion = (post) => post.status === "accepted" ? (post.acceptedVersion ?? post.version) : post.version;
60
+ // A variant as it stood at one of its versions (current or retained), or null
61
+ // when that version is gone.
62
+ export function postAtVersion(post, version) {
63
+ if (version === post.version)
64
+ return post;
65
+ const old = post.history.find((h) => h.version === version);
66
+ if (!old)
67
+ return null;
68
+ const { from: _f, prompt: _p, author: _a, ...rest } = post;
69
+ return {
70
+ ...rest,
71
+ version: old.version,
72
+ title: old.title,
73
+ surfaces: old.surfaces,
74
+ updatedAt: old.at,
75
+ ...(old.from === undefined ? {} : { from: old.from }),
76
+ ...(old.prompt === undefined ? {} : { prompt: old.prompt }),
77
+ ...(old.author === undefined ? {} : { author: old.author }),
78
+ };
79
+ }
80
+ // The variant's newest version not after mock version `n` — what the stage
81
+ // showed when a draft or reply was bound to n (the viewer's frameVersion).
82
+ export function versionAt(post, n) {
83
+ const versions = [post.version, ...post.history.map((h) => h.version)];
84
+ const fit = versions.filter((v) => v <= n);
85
+ return fit.length ? Math.max(...fit) : Math.min(...versions);
86
+ }
87
+ // The state a version option names a version in: the ask's state, else the
88
+ // mock's first (`ask` accepts version options without a state only while the
89
+ // mock has one).
90
+ export const optionState = (ask, mock) => ask.state ?? mock.states[0] ?? null;
54
91
  // The viewer's write-in option on every ask. Reserved, so a delivered
55
92
  // `{id: "other", other: true}` choice never shadows an option the agent declared.
56
93
  export const OTHER_ID = "other";
@@ -58,6 +95,26 @@ export const OTHER_ID = "other";
58
95
  // for a reviewer on a phone who would rather not type: like, dislike, or
59
96
  // "more like this". Rides as a part comment, so it travels in the one reply.
60
97
  export const REACTIONS = ["up", "down", "more"];
98
+ // The settings key prefix of a project's area order (`areaOrder:<project>`): a
99
+ // JSON array of area names, the order Home lists them in.
100
+ export const AREA_ORDER_SETTING = "areaOrder";
101
+ // The state's own render, when a state has situations.
102
+ export const NORMAL_SITUATION = "Normal";
103
+ // The state a situation is stored as: what feedback, decisions and export name.
104
+ export const situationKey = (state, situation) => `${state} · ${situation}`;
105
+ // The state a situation belongs to, or the state itself.
106
+ export function momentOf(situations, state) {
107
+ if (state === null)
108
+ return null;
109
+ for (const [moment, names] of Object.entries(situations)) {
110
+ if (names.some((n) => situationKey(moment, n) === state))
111
+ return moment;
112
+ }
113
+ return state;
114
+ }
115
+ // The states that are moments of the mock, not situations of one.
116
+ export const momentsOf = (states, situations) => states.filter((st) => momentOf(situations, st) === st);
117
+ export const sameScreen = (a, b) => a.mock === b.mock && a.state === b.state;
61
118
  export const HISTORY_LIMIT = 20;
62
119
  // "user" is the reserved trust label for genuine human comments. A session
63
120
  // agent with that name could otherwise have its programmatic comments delivered
@@ -152,15 +209,18 @@ export const htmlSurface = (html, kits) => ({
152
209
  // An open ask is one no reply has answered yet: a write-in or a note alone
153
210
  // answers it too ("Neither, because…").
154
211
  export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
155
- export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a));
156
- // How many built-in "Which one?" asks the viewer shows: one mock-wide when every
212
+ export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a) && !a.dismissed);
213
+ // How many built-in "Which variant?" questions the viewer shows: one mock-wide when every
157
214
  // state needs one and the live names line up, else one per needing state. Same
158
215
  // rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
159
216
  // the question the user actually sees. Answering it archives the losers, which
160
217
  // is what makes it stop counting.
161
218
  export function builtinAskIds(mock, posts, mockId) {
219
+ // Closing the mock closed the choice along with every other open question.
220
+ if (mock.closedAt)
221
+ return [];
162
222
  const live = (state) => posts
163
- .filter((p) => p.mock === mockId && p.state === state && p.status !== "archived")
223
+ .filter((p) => p.mock === mockId && p.state === state && isLive(p.status))
164
224
  .map((p) => p.variant);
165
225
  const needs = (state) => {
166
226
  const names = live(state);
@@ -196,7 +256,7 @@ export function draftedCount(mock, posts) {
196
256
  };
197
257
  return open.filter(said).length;
198
258
  }
199
- // The viewer's built-in "Which one?" for variants no agent ask binds: `variant`
259
+ // The viewer's built-in "Which variant?" for variants no agent ask binds: `variant`
200
260
  // mock-wide, `variant:<state>` per state. Never stored; synthesized from the
201
261
  // variants so a reply answering it validates, flips and reads like any ask. An
202
262
  // option's id is the variant name, so the viewer needs no id mapping.
@@ -217,7 +277,7 @@ export function builtinAsk(mock, posts, id) {
217
277
  return undefined;
218
278
  return {
219
279
  id,
220
- text: "Which one?",
280
+ text: "Which variant?",
221
281
  scope: state === null ? "mock" : "state",
222
282
  ...(state === null ? {} : { state }),
223
283
  options: names.map((v) => ({ id: v, label: v, variant: v })),
@@ -266,7 +326,7 @@ export function surfacesByteLength(surfaces) {
266
326
  let n = 0;
267
327
  for (const p of surfaces) {
268
328
  if (p.kind === "html")
269
- n += p.html.length;
329
+ n += p.html.length + (p.poster?.length ?? 0);
270
330
  else if (p.kind === "diff") {
271
331
  n += p.patch?.length ?? 0;
272
332
  for (const f of p.files ?? [])
@@ -294,7 +354,8 @@ export function surfacesByteLength(surfaces) {
294
354
  }
295
355
  return n;
296
356
  }
297
- // Collect the asset ids an ordered surfaces list references (image surfaces).
357
+ // Collect the asset ids an ordered surfaces list references (image surfaces
358
+ // and html posters).
298
359
  // Used to keep referenced assets out of eviction's first wave. Assets embedded
299
360
  // by raw URL inside html markup are invisible here — touch-on-serve keeps those
300
361
  // warm instead.
@@ -302,6 +363,8 @@ export function collectAssetIds(surfaces, out) {
302
363
  for (const p of surfaces) {
303
364
  if (p.kind === "image")
304
365
  out.add(p.assetId);
366
+ else if (p.kind === "html" && p.poster)
367
+ out.add(p.poster);
305
368
  }
306
369
  }
307
370
  // The images a draft, a reply or a sent ask carries (all keep the user's
@@ -0,0 +1,2 @@
1
+ export declare const VIEWER_CLIENT = "viewer";
2
+ export declare function isViewerRequest(request: Request): boolean;
@@ -0,0 +1,14 @@
1
+ // Whether a request comes from the trusted viewer, the page this server
2
+ // served: only it may act as the user (drafts, replies, user comments).
3
+ // Fetch Metadata says so directly, but browsers omit it on plain http away
4
+ // from localhost (a LAN or tailnet address), where the viewer then looked
5
+ // like any other client and every draft call failed. The viewer also names
6
+ // itself with a custom header, which a cross-origin caller cannot send: that
7
+ // header forces a CORS preflight, and this server answers none, so a sandboxed
8
+ // surface (opaque origin) or another site is refused by the browser itself.
9
+ export const VIEWER_CLIENT = "viewer";
10
+ export function isViewerRequest(request) {
11
+ if (request.headers.get("sec-fetch-site") === "same-origin")
12
+ return true;
13
+ return request.headers.get("x-mockpit-client") === VIEWER_CLIENT;
14
+ }