@kumwe/studio-testkit 0.1.0-alpha.7 → 0.1.0-beta.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.
Files changed (189) hide show
  1. package/README.md +122 -23
  2. package/THIRD_PARTY_NOTICES.md +9 -0
  3. package/conformance/authoring-web/non-drag-move-equivalence.json +199 -0
  4. package/conformance/renderer-web/blob-default-deny.json +62 -0
  5. package/conformance/renderer-web/chart-fallback.json +54 -0
  6. package/conformance/renderer-web/complete-catalog.json +649 -0
  7. package/conformance/renderer-web/escaped-heading.json +43 -0
  8. package/conformance/renderer-web/interactive-behaviors.json +284 -0
  9. package/conformance/renderer-web/presentation-capabilities.json +104 -0
  10. package/conformance/renderer-web/responsive-grid.json +48 -0
  11. package/conformance/renderer-web/security-fallbacks.json +184 -0
  12. package/conformance/rich-text/advanced.first-party.json +67 -0
  13. package/corpus-manifest.json +571 -37
  14. package/dist/contextual-conformance.d.ts +81 -0
  15. package/dist/contextual-conformance.d.ts.map +1 -0
  16. package/dist/contextual-conformance.js +1 -0
  17. package/dist/host-testbed.d.ts +68 -5
  18. package/dist/host-testbed.d.ts.map +1 -1
  19. package/dist/host-testbed.js +1 -547
  20. package/dist/http-authoring-contract.d.ts +3 -0
  21. package/dist/http-authoring-contract.d.ts.map +1 -0
  22. package/dist/http-authoring-contract.js +1 -0
  23. package/dist/http-host-adapter.d.ts +12 -64
  24. package/dist/http-host-adapter.d.ts.map +1 -1
  25. package/dist/http-host-adapter.js +1 -256
  26. package/dist/http-host-responder.d.ts +58 -0
  27. package/dist/http-host-responder.d.ts.map +1 -0
  28. package/dist/http-host-responder.js +1 -0
  29. package/dist/index.d.ts +88 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -140
  32. package/dist/web-conformance.d.ts +112 -0
  33. package/dist/web-conformance.d.ts.map +1 -0
  34. package/dist/web-conformance.js +1 -0
  35. package/fixtures/authoring-message-catalog.en.json +1093 -0
  36. package/fixtures/authoring-save.plan.example.json +48 -0
  37. package/fixtures/authoring-session.example.json +212 -0
  38. package/fixtures/authoring-target.example.json +34 -0
  39. package/fixtures/host-capabilities.example.json +2 -2
  40. package/fixtures/host-operations.example.json +77 -0
  41. package/fixtures/plugin.example.json +12 -0
  42. package/fixtures/preview.render.example.json +1 -0
  43. package/fixtures/reusable-content-type.example.json +26 -0
  44. package/fixtures/studio-config.example.json +3 -3
  45. package/fixtures/studio-deployment.hosted.example.json +152 -0
  46. package/fixtures/studio-deployment.standalone.example.json +9 -0
  47. package/invalid/authoring-save.plan-missing-coordinates.json +16 -0
  48. package/invalid/authoring-save.plan-missing-successor-context.json +28 -0
  49. package/invalid/authoring-save.request-plan-missing-successor-context.json +26 -0
  50. package/invalid/authoring-session.missing-entry-state.json +9 -0
  51. package/invalid/authoring-target.missing-resource-types.json +24 -0
  52. package/invalid/binding-projection-vector.unknown-status.json +85 -0
  53. package/invalid/host-capabilities.invalid-protocol-version.json +1 -1
  54. package/invalid/host-error.retry-delay-without-retry.json +16 -0
  55. package/invalid/host-error.retry-delay-wrong-category.json +16 -0
  56. package/invalid/host-error.revision-without-conflict.json +16 -0
  57. package/invalid/host-sequence.pending-expectation.json +52 -0
  58. package/invalid/host-sequence.preview-render-missing-request-id.json +50 -0
  59. package/invalid/preview-message.activated-unknown-interaction.json +1 -1
  60. package/invalid/preview-message.duplicate-marker.json +24 -0
  61. package/invalid/preview-message.invalid-digest.json +1 -0
  62. package/invalid/preview-message.invalid-marker-grammar.json +19 -0
  63. package/invalid/preview-message.missing-marker-map.json +20 -0
  64. package/invalid/preview-message.missing-render-request-id.json +18 -0
  65. package/invalid/preview-message.unknown-type.json +13 -0
  66. package/invalid/preview-message.viewport-height-too-small.json +13 -0
  67. package/invalid/preview-message.viewport-unsafe-role.json +13 -0
  68. package/invalid/reusable-content-type.entry-values.json +32 -0
  69. package/invalid/studio-chart.script-config.json +10 -0
  70. package/invalid/studio-deployment.long-lived-token.json +24 -0
  71. package/invalid/studio-deployment.missing-required.json +5 -0
  72. package/invalid/studio-deployment.unknown-member.json +15 -0
  73. package/invalid/studio-drawing.svg-payload.json +10 -0
  74. package/invalid/studio-money.float.json +8 -0
  75. package/invalid/studio-presentation.css-injection.json +8 -0
  76. package/invalid/studio-table.html-cell.json +8 -0
  77. package/package.json +8 -4
  78. package/studio-release.json +35 -0
  79. package/vectors/authoring-http/transport-matrix.json +237 -0
  80. package/vectors/binding-projection/app-content-fields.resolved.json +286 -0
  81. package/vectors/binding-projection/field-removed.diagnostic.json +135 -0
  82. package/vectors/binding-projection/model-revision-mismatch.diagnostic.json +124 -0
  83. package/vectors/host/artifact.dependencies.result.json +2 -2
  84. package/vectors/host/artifact.load.stored.json +2 -2
  85. package/vectors/host/artifact.load.unknown.error.json +2 -2
  86. package/vectors/host/artifact.publish.accepted.json +2 -2
  87. package/vectors/host/artifact.publish.forbidden.error.json +1 -1
  88. package/vectors/host/artifact.publish.stale.error.json +2 -2
  89. package/vectors/host/artifact.save.accepted.json +2 -2
  90. package/vectors/host/artifact.save.forbidden.error.json +1 -1
  91. package/vectors/host/artifact.save.stale.error.json +2 -2
  92. package/vectors/host/artifact.unpublish.stale.error.json +2 -2
  93. package/vectors/host/envelope.malformed-context.error.json +2 -2
  94. package/vectors/host/envelope.protocol-version.error.json +1 -1
  95. package/vectors/host/envelope.stale-generation.error.json +2 -2
  96. package/vectors/host/localization.messages.unknown-locale.error.json +2 -2
  97. package/vectors/host/media.abort-upload.unknown.error.json +1 -1
  98. package/vectors/host/media.authorize-upload.granted.json +1 -1
  99. package/vectors/host/media.authorize-upload.path-filename.error.json +1 -1
  100. package/vectors/host/media.authorize-upload.too-large.error.json +1 -1
  101. package/vectors/host/media.complete-upload.unknown.error.json +1 -1
  102. package/vectors/host/media.get.unknown.json +2 -2
  103. package/vectors/host/media.import-external.private-host.error.json +1 -1
  104. package/vectors/host/media.list.cursor.error.json +2 -2
  105. package/vectors/host/media.list.limit.error.json +2 -2
  106. package/vectors/host/media.upload-status.unknown.error.json +1 -1
  107. package/vectors/host/model.get.stored.json +37 -0
  108. package/vectors/host/model.list.authorized.json +31 -0
  109. package/vectors/host/permission.explain.withheld.json +2 -2
  110. package/vectors/host/permission.refresh.snapshot.json +2 -2
  111. package/vectors/host/recovery.load.absent.json +2 -2
  112. package/vectors/host/telemetry.emit.accepted.json +2 -2
  113. package/vectors/host/telemetry.emit.non-primitive.error.json +2 -2
  114. package/vectors/host-sequence/artifact.publish.changed-intent.sequence.json +75 -0
  115. package/vectors/host-sequence/artifact.publish.idempotent-replay.sequence.json +111 -0
  116. package/vectors/host-sequence/preview.cancel.cross-context.sequence.json +78 -0
  117. package/vectors/host-sequence/preview.render.cancelled.sequence.json +75 -0
  118. package/vectors/host-sequence/recovery.store.canonical-number.sequence.json +72 -0
  119. package/vectors/host-sequence/recovery.store.changed-context.sequence.json +61 -0
  120. package/vectors/host-sequence/recovery.store.rate-limited.sequence.json +93 -0
  121. package/vectors/host-sequence/recovery.store.resource-scope.sequence.json +61 -0
  122. package/vectors/host-sequence/recovery.store.wrong-operation-id.sequence.json +56 -0
  123. package/vectors/preview/canonical-preorder.json +102 -0
  124. package/vectors/preview/empty-draft.json +36 -0
  125. package/vectors/schema-profile/alternative-limit.accepted.json +95 -0
  126. package/vectors/schema-profile/alternative-limit.rejected.json +92 -0
  127. package/vectors/schema-profile/array-composition.accepted.json +35 -0
  128. package/vectors/schema-profile/byte-limit.accepted.json +30 -0
  129. package/vectors/schema-profile/byte-limit.rejected.json +26 -0
  130. package/vectors/schema-profile/closed-object.accepted.json +38 -0
  131. package/vectors/schema-profile/decimal-multiple.accepted.json +54 -0
  132. package/vectors/schema-profile/depth-limit.accepted.json +90 -0
  133. package/vectors/schema-profile/depth-limit.rejected.json +88 -0
  134. package/vectors/schema-profile/description-limit.accepted.json +29 -0
  135. package/vectors/schema-profile/description-limit.rejected.json +25 -0
  136. package/vectors/schema-profile/diagnostic-precedence.rejected.json +26 -0
  137. package/vectors/schema-profile/duplicate-dependent-required.rejected.json +21 -0
  138. package/vectors/schema-profile/duplicate-required.rejected.json +18 -0
  139. package/vectors/schema-profile/empty-combinator.rejected.json +17 -0
  140. package/vectors/schema-profile/empty-enum.rejected.json +17 -0
  141. package/vectors/schema-profile/enum-limit.accepted.json +89 -0
  142. package/vectors/schema-profile/enum-limit.rejected.json +84 -0
  143. package/vectors/schema-profile/examples-limit.accepted.json +35 -0
  144. package/vectors/schema-profile/examples-limit.rejected.json +31 -0
  145. package/vectors/schema-profile/format-keyword.rejected.json +17 -0
  146. package/vectors/schema-profile/forward-reference-path-precedence.rejected.json +20 -0
  147. package/vectors/schema-profile/invalid-pointer.rejected.json +18 -0
  148. package/vectors/schema-profile/json-depth-limit.accepted.json +278 -0
  149. package/vectors/schema-profile/json-depth-limit.rejected.json +276 -0
  150. package/vectors/schema-profile/json-item-limit.accepted.json +686 -0
  151. package/vectors/schema-profile/json-item-limit.rejected.json +682 -0
  152. package/vectors/schema-profile/json-property-limit.accepted.json +1031 -0
  153. package/vectors/schema-profile/json-property-limit.rejected.json +1028 -0
  154. package/vectors/schema-profile/local-reference.accepted.json +28 -0
  155. package/vectors/schema-profile/memoized-diagnostic.accepted.json +31 -0
  156. package/vectors/schema-profile/node-limit.accepted.json +1097 -0
  157. package/vectors/schema-profile/node-limit.rejected.json +1094 -0
  158. package/vectors/schema-profile/non-object-root.rejected.json +9 -0
  159. package/vectors/schema-profile/non-schema-reference.rejected.json +21 -0
  160. package/vectors/schema-profile/object-key-limit.accepted.json +32 -0
  161. package/vectors/schema-profile/object-key-limit.rejected.json +28 -0
  162. package/vectors/schema-profile/open-root.rejected.json +13 -0
  163. package/vectors/schema-profile/percent-encoded-pointer.rejected.json +26 -0
  164. package/vectors/schema-profile/property-name-limit.accepted.json +543 -0
  165. package/vectors/schema-profile/property-name-limit.rejected.json +536 -0
  166. package/vectors/schema-profile/recursive-diagnostic-precedence.rejected.json +30 -0
  167. package/vectors/schema-profile/recursive-reference.rejected.json +24 -0
  168. package/vectors/schema-profile/reference-fanout.accepted.json +662 -0
  169. package/vectors/schema-profile/reference-length-limit.accepted.json +45 -0
  170. package/vectors/schema-profile/reference-length-limit.rejected.json +39 -0
  171. package/vectors/schema-profile/reference-limit.accepted.json +416 -0
  172. package/vectors/schema-profile/reference-limit.rejected.json +415 -0
  173. package/vectors/schema-profile/reference-vs-keyword-precedence.rejected.json +22 -0
  174. package/vectors/schema-profile/reserved-pointer.rejected.json +26 -0
  175. package/vectors/schema-profile/root-diagnostic-precedence.rejected.json +16 -0
  176. package/vectors/schema-profile/root-vs-reference-precedence.rejected.json +19 -0
  177. package/vectors/schema-profile/schema-map-limit.accepted.json +539 -0
  178. package/vectors/schema-profile/schema-map-limit.rejected.json +536 -0
  179. package/vectors/schema-profile/semantic-recursion-precedence.rejected.json +21 -0
  180. package/vectors/schema-profile/semantic-reference-precedence.rejected.json +21 -0
  181. package/vectors/schema-profile/title-limit.accepted.json +29 -0
  182. package/vectors/schema-profile/title-limit.rejected.json +25 -0
  183. package/vectors/schema-profile/unresolved-reference.rejected.json +17 -0
  184. package/vectors/schema-profile/unsafe-dependent-required.rejected.json +24 -0
  185. package/vectors/schema-profile/unsafe-member.rejected.json +17 -0
  186. package/vectors/schema-profile/unsafe-required.rejected.json +17 -0
  187. package/dist/host-testbed.js.map +0 -1
  188. package/dist/http-host-adapter.js.map +0 -1
  189. package/dist/index.js.map +0 -1
@@ -1,547 +1 @@
1
- import { validateExternalUrl } from '@kumwe/studio-core';
2
- import { STUDIO_CONTRACT_VERSION, STUDIO_WIRE_PROTOCOL_VERSION, } from '@kumwe/studio-protocol';
3
- export class TestbedHostError extends Error {
4
- error;
5
- constructor(error) {
6
- super(error.message.defaultMessage ?? error.message.key);
7
- this.name = 'TestbedHostError';
8
- this.error = error;
9
- }
10
- }
11
- export function createHostRequestContextFixture(options = {}) {
12
- return {
13
- ...(options.expectedRevision === undefined
14
- ? {}
15
- : { expectedRevision: options.expectedRevision }),
16
- ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
17
- ...(options.locale === undefined ? {} : { locale: options.locale }),
18
- operationId: options.operationId ?? 'studio.test/operation',
19
- protocolVersion: options.protocolVersion ?? STUDIO_WIRE_PROTOCOL_VERSION,
20
- requestId: options.requestId ?? 'requests/test-1',
21
- resourceContextKey: options.resourceContextKey ?? 'contexts/test',
22
- sessionGeneration: options.sessionGeneration ?? 'session-r1',
23
- };
24
- }
25
- /**
26
- * The permissions this reference host gates artifact mutations behind. A host
27
- * chooses its own names; what the contract fixes is that a mutation is
28
- * authorized and that a withheld permission is refused as `forbidden`.
29
- */
30
- const SAVE_PERMISSION = 'studio.permission/save';
31
- const PUBLISH_PERMISSION = 'studio.permission/publish';
32
- /** Deterministic bounds the reference host authorizes uploads within. */
33
- const UPLOAD_CHUNK_BYTES = 5_242_880;
34
- const UPLOAD_MAXIMUM_BYTES = 52_428_800;
35
- const GRANT_EXPIRY = '2030-01-01T00:00:00Z';
36
- export function createTestbedHost(options = {}) {
37
- const artifactStore = new Map();
38
- for (const seeded of options.documents ?? []) {
39
- const document = cloneValue(seeded);
40
- const revision = `${document.id}-r1`;
41
- document.revision = revision;
42
- artifactStore.set(document.id, { document, revision, serial: 1 });
43
- }
44
- const mediaAssets = cloneValue(options.mediaAssets ?? []);
45
- const resources = cloneValue(options.resources ?? []);
46
- const messageBundles = new Map(Object.entries(cloneValue(options.messages ?? {})));
47
- const recoveryStore = new Map();
48
- const telemetryEvents = [];
49
- const injectedFailures = [];
50
- const renderCallback = options.render ?? defaultRender;
51
- const uploadGrants = new Map();
52
- const acceptedUploads = new Map();
53
- let uploadSerial = 0;
54
- let permissions = (options.permissions ?? []).slice();
55
- let generationSerial = 1;
56
- let connected = true;
57
- let correlationSerial = 0;
58
- let importSerial = 0;
59
- function currentGeneration() {
60
- return `session-r${generationSerial}`;
61
- }
62
- function createError(category, defaultMessage, extras = {}) {
63
- correlationSerial += 1;
64
- return {
65
- category,
66
- contractVersion: STUDIO_CONTRACT_VERSION,
67
- correlationId: `testbed-correlation-${correlationSerial}`,
68
- kind: 'host-error',
69
- message: { defaultMessage, key: `studio.testbed/${category}` },
70
- retryable: extras.retryable ?? (category === 'rate-limited' || category === 'unavailable'),
71
- ...(extras.revision === undefined ? {} : { revision: extras.revision }),
72
- };
73
- }
74
- function fail(category, defaultMessage, extras = {}) {
75
- throw new TestbedHostError(createError(category, defaultMessage, extras));
76
- }
77
- function guard(portName, operation, context) {
78
- if (!connected) {
79
- fail('unavailable', 'The testbed host is disconnected.');
80
- }
81
- const index = injectedFailures.findIndex((entry) => entry.portName === portName && entry.operation === operation);
82
- if (index >= 0) {
83
- const injected = injectedFailures[index];
84
- injectedFailures.splice(index, 1);
85
- if (injected !== undefined) {
86
- fail(injected.category, 'The testbed injected this failure.');
87
- }
88
- }
89
- if (!isNonEmptyString(context.requestId) ||
90
- !isNonEmptyString(context.operationId) ||
91
- !isNonEmptyString(context.resourceContextKey) ||
92
- !isNonEmptyString(context.sessionGeneration)) {
93
- fail('invalid-request', 'The request context is structurally invalid.');
94
- }
95
- if (context.protocolVersion !== STUDIO_WIRE_PROTOCOL_VERSION) {
96
- fail('incompatible', 'The request protocol version is not supported by this testbed.');
97
- }
98
- if (context.sessionGeneration !== currentGeneration()) {
99
- fail('invalid-request', 'The session generation is no longer valid.');
100
- }
101
- }
102
- function run(portName, operation, context, body) {
103
- try {
104
- guard(portName, operation, context);
105
- return Promise.resolve(body());
106
- }
107
- catch (error) {
108
- if (error instanceof TestbedHostError) {
109
- return Promise.reject(error);
110
- }
111
- return Promise.reject(new TestbedHostError(createError('internal', 'The testbed operation failed unexpectedly.')));
112
- }
113
- }
114
- /**
115
- * The authorization gate a production host applies to every mutation. Reads
116
- * are authorized by the resolved request context; mutations additionally
117
- * require the host-declared permission for the operation, and a withheld
118
- * permission is refused without disclosing whether the artifact exists.
119
- */
120
- function requireArtifactPermission(permission) {
121
- if (!permissions.includes(permission)) {
122
- fail('forbidden', 'The session does not hold the permission this operation requires.');
123
- }
124
- }
125
- function requireStored(id) {
126
- const stored = artifactStore.get(id);
127
- if (stored === undefined) {
128
- fail('not-found', 'The requested artifact is not available.');
129
- }
130
- return stored;
131
- }
132
- function ensureExpectedRevision(stored, context) {
133
- if (context.expectedRevision !== stored.revision) {
134
- fail('conflict', 'The stored artifact revision differs from the expected revision.', {
135
- revision: stored.revision,
136
- });
137
- }
138
- }
139
- function advanceRevision(stored) {
140
- stored.serial += 1;
141
- stored.revision = `${stored.document.id}-r${stored.serial}`;
142
- stored.document.revision = stored.revision;
143
- return stored.revision;
144
- }
145
- function setStatus(id, context, status) {
146
- requireArtifactPermission(PUBLISH_PERMISSION);
147
- const stored = requireStored(id);
148
- ensureExpectedRevision(stored, context);
149
- stored.document.status = status;
150
- return { revision: advanceRevision(stored), value: null };
151
- }
152
- function resolveStart(cursor) {
153
- if (cursor === undefined) {
154
- return 0;
155
- }
156
- const start = decodeCursor(cursor);
157
- if (start === undefined) {
158
- fail('invalid-request', 'The pagination cursor is not valid.');
159
- }
160
- return start;
161
- }
162
- function ensureLimit(limit) {
163
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
164
- fail('invalid-request', 'The limit must be an integer between 1 and 100.');
165
- }
166
- }
167
- const host = {
168
- artifact: {
169
- dependencies(reference, context) {
170
- return run('artifact', 'dependencies', context, () => ({ value: [] }));
171
- },
172
- load(reference, context) {
173
- return run('artifact', 'load', context, () => {
174
- const stored = requireStored(reference.id);
175
- return { revision: stored.revision, value: cloneValue(stored.document) };
176
- });
177
- },
178
- publish(reference, context) {
179
- return run('artifact', 'publish', context, () => setStatus(reference.id, context, 'published'));
180
- },
181
- save(document, context) {
182
- return run('artifact', 'save', context, () => {
183
- requireArtifactPermission(SAVE_PERMISSION);
184
- const stored = requireStored(document.id);
185
- ensureExpectedRevision(stored, context);
186
- stored.document = cloneValue(document);
187
- return { revision: advanceRevision(stored), value: null };
188
- });
189
- },
190
- unpublish(reference, context) {
191
- return run('artifact', 'unpublish', context, () => setStatus(reference.id, context, 'draft'));
192
- },
193
- },
194
- localization: {
195
- messages(locale, namespaces, context) {
196
- return run('localization', 'messages', context, () => {
197
- const bundle = messageBundles.get(locale);
198
- if (bundle === undefined) {
199
- fail('not-found', 'The requested locale is not available.');
200
- }
201
- const value = {};
202
- for (const [key, message] of Object.entries(bundle)) {
203
- const qualifiedKey = key;
204
- if (namespaces.some((namespace) => qualifiedKey === namespace || qualifiedKey.startsWith(`${namespace}.`))) {
205
- value[qualifiedKey] = message;
206
- }
207
- }
208
- return { value };
209
- });
210
- },
211
- },
212
- media: {
213
- abortUpload(uploadId, context) {
214
- return run('media', 'abort-upload', context, () => {
215
- if (!uploadGrants.delete(uploadId)) {
216
- fail('not-found', 'The requested upload is not available.');
217
- }
218
- return { value: null };
219
- });
220
- },
221
- authorizeUpload(request, context) {
222
- return run('media', 'authorize-upload', context, () => {
223
- assertUploadRequest(request);
224
- uploadSerial += 1;
225
- const uploadId = `uploads/testbed-${String(uploadSerial)}`;
226
- const grant = {
227
- expiresAt: GRANT_EXPIRY,
228
- headers: { 'X-Upload-Session': uploadId },
229
- method: 'PUT',
230
- plan: {
231
- chunkBytes: UPLOAD_CHUNK_BYTES,
232
- maximumBytes: UPLOAD_MAXIMUM_BYTES,
233
- resumable: true,
234
- },
235
- uploadId,
236
- url: `https://uploads.testbed.invalid/${uploadId}`,
237
- };
238
- uploadGrants.set(uploadId, { grant, request: cloneValue(request) });
239
- return { value: cloneValue(grant) };
240
- });
241
- },
242
- completeUpload(uploadId, context) {
243
- return run('media', 'complete-upload', context, () => {
244
- const pending = uploadGrants.get(uploadId);
245
- if (pending === undefined) {
246
- fail('not-found', 'The requested upload is not available.');
247
- }
248
- uploadGrants.delete(uploadId);
249
- // The host verifies what it received; a declared media type is
250
- // never trusted, so acceptance lands in `processing`.
251
- const accepted = {
252
- id: `assets/${uploadId.replace('uploads/', '')}`,
253
- revision: `${uploadId}-r1`,
254
- state: 'processing',
255
- };
256
- acceptedUploads.set(accepted.id, accepted);
257
- return { revision: accepted.revision, value: cloneValue(accepted) };
258
- });
259
- },
260
- get(assetId, context) {
261
- return run('media', 'get', context, () => {
262
- const asset = mediaAssets.find((entry) => entry.id === assetId);
263
- const result = asset === undefined
264
- ? { value: null }
265
- : { revision: asset.revision, value: cloneValue(asset) };
266
- return result;
267
- });
268
- },
269
- list(query, context) {
270
- return run('media', 'list', context, () => {
271
- ensureLimit(query.limit);
272
- const search = query.search?.toLowerCase();
273
- const filtered = mediaAssets.filter((asset) => {
274
- if (query.mediaTypes !== undefined && !query.mediaTypes.includes(asset.mediaType)) {
275
- return false;
276
- }
277
- return search === undefined || asset.filename.toLowerCase().includes(search);
278
- });
279
- const start = resolveStart(query.cursor);
280
- const nextIndex = start + query.limit;
281
- return {
282
- value: {
283
- assets: cloneValue(filtered.slice(start, nextIndex)),
284
- ...(nextIndex < filtered.length ? { nextCursor: encodeCursor(nextIndex) } : {}),
285
- },
286
- };
287
- });
288
- },
289
- importExternal(url, context) {
290
- return run('media', 'import-external', context, () => {
291
- const verdict = validateExternalUrl(url);
292
- if (!verdict.ok) {
293
- // The stable reason is disclosed; the candidate never is.
294
- fail('validation-failed', `The external media candidate was refused: ${verdict.reason}.`);
295
- }
296
- importSerial += 1;
297
- const accepted = {
298
- id: `assets/imported-${String(importSerial)}`,
299
- revision: `imports/testbed-${String(importSerial)}-r1`,
300
- state: 'processing',
301
- };
302
- acceptedUploads.set(accepted.id, accepted);
303
- return { revision: accepted.revision, value: cloneValue(accepted) };
304
- });
305
- },
306
- uploadStatus(assetId, context) {
307
- return run('media', 'upload-status', context, () => {
308
- const accepted = acceptedUploads.get(assetId);
309
- if (accepted === undefined) {
310
- fail('not-found', 'The requested asset is not available.');
311
- }
312
- return { revision: accepted.revision, value: cloneValue(accepted) };
313
- });
314
- },
315
- },
316
- permission: {
317
- explain(operation, context) {
318
- return run('permission', 'explain', context, () => {
319
- const explanation = permissions.includes(operation)
320
- ? { allowed: true }
321
- : {
322
- allowed: false,
323
- reason: {
324
- defaultMessage: 'The session does not hold this permission.',
325
- key: 'studio.testbed/permission-denied',
326
- },
327
- };
328
- return { value: explanation };
329
- });
330
- },
331
- refresh(context) {
332
- return run('permission', 'refresh', context, () => ({
333
- value: { permissions: permissions.slice(), sessionGeneration: currentGeneration() },
334
- }));
335
- },
336
- },
337
- preview: {
338
- cancel(draftDigest, context) {
339
- return run('preview', 'cancel', context, () => ({ value: null }));
340
- },
341
- render(payload, context) {
342
- return run('preview', 'render', context, () => ({
343
- value: cloneValue(renderCallback(cloneValue(payload))),
344
- }));
345
- },
346
- },
347
- recovery: {
348
- discard(context) {
349
- return run('recovery', 'discard', context, () => {
350
- recoveryStore.delete(context.resourceContextKey);
351
- return { value: null };
352
- });
353
- },
354
- load(context) {
355
- return run('recovery', 'load', context, () => {
356
- const envelope = recoveryStore.get(context.resourceContextKey);
357
- const result = envelope === undefined ? { value: null } : { value: cloneValue(envelope) };
358
- return result;
359
- });
360
- },
361
- store(envelope, context) {
362
- return run('recovery', 'store', context, () => {
363
- recoveryStore.set(context.resourceContextKey, cloneValue(envelope));
364
- return { value: null };
365
- });
366
- },
367
- },
368
- resource: {
369
- search(query, context) {
370
- return run('resource', 'search', context, () => {
371
- ensureLimit(query.limit);
372
- const search = query.search?.toLowerCase();
373
- const filtered = resources.filter((hit) => {
374
- if (hit.resourceType !== query.resourceType) {
375
- return false;
376
- }
377
- return (search === undefined ||
378
- (hit.label.defaultMessage?.toLowerCase().includes(search) ?? false));
379
- });
380
- const start = resolveStart(query.cursor);
381
- const nextIndex = start + query.limit;
382
- return {
383
- value: {
384
- items: cloneValue(filtered.slice(start, nextIndex)),
385
- ...(nextIndex < filtered.length ? { nextCursor: encodeCursor(nextIndex) } : {}),
386
- },
387
- };
388
- });
389
- },
390
- },
391
- telemetry: {
392
- emit(event, context) {
393
- return run('telemetry', 'emit', context, () => {
394
- if (event.attributes !== undefined) {
395
- for (const attribute of Object.values(event.attributes)) {
396
- if (!isJsonPrimitiveValue(attribute)) {
397
- fail('invalid-request', 'Telemetry attribute values must be JSON primitives.');
398
- }
399
- }
400
- }
401
- telemetryEvents.push(cloneValue(event));
402
- return { value: null };
403
- });
404
- },
405
- },
406
- };
407
- const controls = {
408
- disconnect() {
409
- connected = false;
410
- },
411
- failNext(portName, operation, category) {
412
- injectedFailures.push({ category, operation, portName });
413
- },
414
- importExternalMedia(candidate, context) {
415
- return run('media', 'import-external', context, () => {
416
- const verdict = validateExternalUrl(candidate);
417
- if (!verdict.ok) {
418
- fail('validation-failed', `The external media source is not allowed by the URL policy (${verdict.reason}).`);
419
- }
420
- importSerial += 1;
421
- const id = `media/import-${importSerial}`;
422
- const revision = `${id}-r1`;
423
- mediaAssets.push({
424
- byteSize: 0,
425
- contractVersion: STUDIO_CONTRACT_VERSION,
426
- filename: `import-${importSerial}`,
427
- id,
428
- kind: 'media-asset',
429
- mediaKind: 'other',
430
- mediaType: 'application/octet-stream',
431
- metadata: {},
432
- revision,
433
- state: 'processing',
434
- });
435
- return { revision, value: { id, revision, state: 'processing' } };
436
- });
437
- },
438
- reconnect() {
439
- connected = true;
440
- },
441
- revisionOf(id) {
442
- return artifactStore.get(id)?.revision;
443
- },
444
- get sessionGeneration() {
445
- return currentGeneration();
446
- },
447
- setPermissions(next) {
448
- permissions = next.slice();
449
- generationSerial += 1;
450
- },
451
- get telemetryEvents() {
452
- return cloneValue(telemetryEvents);
453
- },
454
- };
455
- return { controls, host };
456
- }
457
- function defaultRender(payload) {
458
- return { diagnostics: [], draftDigest: payload.draftDigest, markers: [] };
459
- }
460
- function isNonEmptyString(value) {
461
- return typeof value === 'string' && value.length > 0;
462
- }
463
- function isJsonPrimitiveValue(value) {
464
- return (value === null ||
465
- typeof value === 'boolean' ||
466
- typeof value === 'string' ||
467
- (typeof value === 'number' && Number.isFinite(value)));
468
- }
469
- const CURSOR_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
470
- function encodeCursor(index) {
471
- const text = `index:${index}`;
472
- let encoded = '';
473
- for (let position = 0; position < text.length; position += 3) {
474
- const first = text.charCodeAt(position);
475
- const second = position + 1 < text.length ? text.charCodeAt(position + 1) : undefined;
476
- const third = position + 2 < text.length ? text.charCodeAt(position + 2) : undefined;
477
- const chunk = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);
478
- encoded += CURSOR_ALPHABET.charAt((chunk >> 18) & 0x3f);
479
- encoded += CURSOR_ALPHABET.charAt((chunk >> 12) & 0x3f);
480
- encoded += second === undefined ? '=' : CURSOR_ALPHABET.charAt((chunk >> 6) & 0x3f);
481
- encoded += third === undefined ? '=' : CURSOR_ALPHABET.charAt(chunk & 0x3f);
482
- }
483
- return encoded;
484
- }
485
- function decodeCursor(cursor) {
486
- if (cursor.length === 0 || cursor.length % 4 !== 0) {
487
- return undefined;
488
- }
489
- const paddingLength = cursor.endsWith('==') ? 2 : cursor.endsWith('=') ? 1 : 0;
490
- const body = cursor.slice(0, cursor.length - paddingLength);
491
- let bits = 0;
492
- let bitCount = 0;
493
- let text = '';
494
- for (const character of body) {
495
- const sextet = CURSOR_ALPHABET.indexOf(character);
496
- if (sextet < 0) {
497
- return undefined;
498
- }
499
- bits = (bits << 6) | sextet;
500
- bitCount += 6;
501
- if (bitCount >= 8) {
502
- bitCount -= 8;
503
- text += String.fromCharCode((bits >> bitCount) & 0xff);
504
- }
505
- }
506
- const match = /^index:(0|[1-9][0-9]*)$/u.exec(text);
507
- const digits = match?.[1];
508
- if (digits === undefined || digits.length > 9) {
509
- return undefined;
510
- }
511
- return Number(digits);
512
- }
513
- function cloneValue(value) {
514
- return JSON.parse(JSON.stringify(value));
515
- }
516
- /**
517
- * The policy gate that runs before any byte moves: a declared upload larger
518
- * than the host's bound, or one carrying a filename the canonical shape
519
- * refuses, is rejected at authorization rather than after a transfer.
520
- */
521
- function assertUploadRequest(request) {
522
- if (request.byteSize > UPLOAD_MAXIMUM_BYTES) {
523
- throw new TestbedHostError({
524
- category: 'limit-exceeded',
525
- contractVersion: STUDIO_CONTRACT_VERSION,
526
- kind: 'host-error',
527
- message: {
528
- defaultMessage: 'The declared upload exceeds the accepted size.',
529
- key: 'studio.testbed/limit-exceeded',
530
- },
531
- retryable: false,
532
- });
533
- }
534
- if (request.filename.includes('/') || request.filename.includes('\\')) {
535
- throw new TestbedHostError({
536
- category: 'validation-failed',
537
- contractVersion: STUDIO_CONTRACT_VERSION,
538
- kind: 'host-error',
539
- message: {
540
- defaultMessage: 'The declared filename is not a canonical display name.',
541
- key: 'studio.testbed/validation-failed',
542
- },
543
- retryable: false,
544
- });
545
- }
546
- }
547
- //# sourceMappingURL=host-testbed.js.map
1
+ import{canonicalStringify as e,validateExternalUrl as t}from"@kumwe/studio-core";import{HostPortFailure as n,STUDIO_CONTRACT_VERSION as r,STUDIO_STALE_SESSION_GENERATION_DIAGNOSTIC_CODE as i,STUDIO_WIRE_PROTOCOL_VERSION as a}from"@kumwe/studio-protocol";export class TestbedHostError extends n{constructor(e){super(e),this.name=`TestbedHostError`}}export function createHostRequestContextFixture(e={}){return{...e.expectedRevision===void 0?{}:{expectedRevision:e.expectedRevision},...e.idempotencyKey===void 0?{}:{idempotencyKey:e.idempotencyKey},...e.locale===void 0?{}:{locale:e.locale},operationId:e.operationId??`studio.test/operation`,protocolVersion:e.protocolVersion??a,requestId:e.requestId??`requests/test-1`,resourceContextKey:e.resourceContextKey??`contexts/test`,sessionGeneration:e.sessionGeneration??`session-r1`}}const c=52428800;export function createTestbedHost(n={}){let s=new Map;for(let e of n.documents??[]){let t=m(e);if(s.has(t.id))throw TypeError(`Testbed artifact ${t.id} is duplicated.`);let n=t.revision;s.set(t.id,{document:t,revision:n,serial:te(t.id,n)})}let l=m(n.mediaAssets??[]),f=m(n.resources??[]),_=m(n.authoring?.targets??[]),v=m(n.authoring?.types??[]),y=m(n.authoring?.starts??[]),b=m(n.authoring?.plans??[]),x=m(n.authoring?.saves??[]),S=[];g(`target`,_.map(e=>e.request)),g(`start`,y.map(e=>e.request)),g(`plan`,b.map(e=>e.intent)),g(`save`,x.map(e=>e.request)),g(`type`,v.map(e=>({reference:e.definition,resourceContextKey:e.resourceContextKey,targetId:e.targetId})));let ae=new Map(Object.entries(m(n.messages??{}))),C=new Map,oe=[],w=[],se=n.render??u,T=new Map,E=new Map,D=[],O=new Map,k=new Map;for(let e of n.rateLimits??[]){if(!Number.isSafeInteger(e.maximumRequests)||e.maximumRequests<1||e.maximumRequests>1e3||!Number.isSafeInteger(e.windowMilliseconds)||e.windowMilliseconds<1||e.windowMilliseconds>864e5)throw RangeError(`Testbed rate limits require positive safe-integer bounds.`);if(O.has(e.operationId))throw TypeError(`Testbed rate limit ${e.operationId} is duplicated.`);O.set(e.operationId,{...e})}let A=new Map,j=new Map,M=0,N=(n.permissions??[]).slice(),P=n.sessionGeneration??`session-r1`;if(!d(P)||P.length>200)throw TypeError(`The testbed session generation must be a non-empty bounded revision.`);let F=te(`session`,P),I=!0,L=0,R=0,z=n.initialClockMilliseconds??0;if(!Number.isSafeInteger(z)||z<0)throw RangeError(`The testbed logical clock must be a non-negative safe integer.`);function B(){return P}function V(e,t,n={}){return L+=1,{category:e,contractVersion:r,correlationId:`testbed-correlation-${L}`,kind:`host-error`,message:{defaultMessage:t,key:`studio.testbed/${e}`},retryable:n.retryable??(e===`rate-limited`||e===`unavailable`),...n.diagnostics===void 0?{}:{diagnostics:m(n.diagnostics)},...n.retryAfterMilliseconds===void 0?{}:{retryAfterMilliseconds:n.retryAfterMilliseconds},...n.revision===void 0?{}:{revision:n.revision}}}function H(e,t,n={}){throw new TestbedHostError(V(e,t,n))}function U(e,t,r){I||H(`unavailable`,`The testbed host is disconnected.`),(!d(r.requestId)||!d(r.operationId)||!d(r.resourceContextKey)||!d(r.sessionGeneration))&&H(`invalid-request`,`The request context is structurally invalid.`),r.protocolVersion!==a&&H(`incompatible`,`The request protocol version is not supported by this testbed.`),r.sessionGeneration!==B()&&H(`invalid-request`,`The session generation is no longer valid.`,{diagnostics:[{code:i,message:{defaultMessage:`The request belongs to an obsolete Studio session generation.`,key:i},severity:`blocking`}]});let o=`studio.operation/${e}.${t}`;r.operationId!==o&&(n.allowTestOperationId!==!0||r.operationId!==`studio.test/operation`)&&H(`invalid-request`,`The request operation identifier does not match the invoked port.`)}function W(e,t,n){let r=w.findIndex(n=>n.portName===e&&n.operation===t);if(r>=0){let e=w[r];w.splice(r,1),e!==void 0&&H(e.category,`The testbed injected this failure.`)}let i=O.get(n.operationId);if(i===void 0)return;let a=k.get(n.operationId);(a===void 0||z>=a.startedAtMilliseconds+i.windowMilliseconds)&&(a={requestCount:0,startedAtMilliseconds:z},k.set(n.operationId,a)),a.requestCount>=i.maximumRequests&&H(`rate-limited`,`The operation has reached its deterministic request bound.`,{retryAfterMilliseconds:a.startedAtMilliseconds+i.windowMilliseconds-z}),a.requestCount+=1}async function G(e){try{return await e()}catch(e){throw e instanceof TestbedHostError?e:new TestbedHostError(V(`internal`,`The testbed operation failed unexpectedly.`))}}function K(e,t,n,r){try{return U(e,t,n),W(e,t,n),G(r)}catch(e){return G(()=>{throw e})}}function q(t,n,r,i,a){try{U(t,n,i);let o=i.idempotencyKey;if(o===void 0)return W(t,n,i),G(a);let s=e({idempotencyKey:o,operationId:i.operationId,resourceContextKey:i.resourceContextKey,sessionGeneration:i.sessionGeneration}),c=e({argument:r,context:{...i.expectedRevision===void 0?{}:{expectedRevision:i.expectedRevision},...i.locale===void 0?{}:{locale:i.locale},protocolVersion:i.protocolVersion}}),l=T.get(s);if(l!==void 0)return l.fingerprint!==c&&H(`invalid-request`,`The idempotency key was already accepted for another intent.`),l.promise.then(e=>m(e));W(t,n,i);let u=G(a).then(e=>m(e));return T.set(s,{fingerprint:c,promise:u}),u.catch(()=>{T.get(s)?.promise===u&&T.delete(s)}),u.then(e=>m(e))}catch(e){return G(()=>{throw e})}}function ce(e){N.includes(e)||H(`forbidden`,`The session does not hold the permission this operation requires.`)}function J(e){let t=s.get(e);return t===void 0&&H(`not-found`,`The requested artifact is not available.`),t}function le(e,t){t.expectedRevision!==e.revision&&H(`conflict`,`The stored artifact revision differs from the expected revision.`,{revision:e.revision})}function ue(e){return e.serial+=1,e.revision=`${e.document.id}-r${e.serial}`,e.document.revision=e.revision,e.revision}function de(e,t,n){ce(`studio.permission/publish`);let r=J(e);return le(r,t),r.document.status=n,{revision:ue(r),value:null}}function Y(e){if(e===void 0)return 0;let t=re(e);return t===void 0&&H(`invalid-request`,`The pagination cursor is not valid.`),t}function X(e){(!Number.isSafeInteger(e)||e<1||e>100)&&H(`invalid-request`,`The limit must be an integer between 1 and 100.`)}function Z(e,t){S.push({argument:m(t),operation:e})}function Q(e,t){e!==t.resourceContextKey&&H(`invalid-request`,`The contextual authoring request does not match the authoritative resource context.`)}function $(t,n,r){let i=e(n),a=t.find(t=>e(r(t))===i);return a===void 0&&H(`not-found`,`The requested contextual authoring fixture is not available.`),a}return{controls:{advanceClock(e){if(!Number.isSafeInteger(e)||e<0)throw RangeError(`Testbed clock advances must be non-negative safe integers.`);let t=z+e;if(!Number.isSafeInteger(t))throw RangeError(`The testbed logical clock cannot exceed the safe-integer bound.`);z=t},get authoringOperations(){return m(S)},artifactStatus(e){return s.get(e)?.document.status},disconnect(){I=!1},failNext(e,t,n){w.push({category:n,operation:t,portName:e})},importExternalMedia(e,n){return K(`media`,`import-external`,n,()=>{let n=t(e);n.ok||H(`validation-failed`,`The external media source is not allowed by the URL policy (${n.reason}).`),R+=1;let i=`media/import-${R}`,a=`${i}-r1`;return l.push({byteSize:0,contractVersion:r,filename:`import-${R}`,id:i,kind:`media-asset`,mediaKind:`other`,mediaType:`application/octet-stream`,metadata:{},revision:a,state:`processing`}),{revision:a,value:{id:i,revision:a,state:`processing`}}})},reconnect(){I=!0},recoveryEnvelope(e){let t=C.get(e);return t===void 0?void 0:m(t)},revisionOf(e){return s.get(e)?.revision},get pendingPreviewRenders(){return E.size},get previewDeliveries(){return D.slice()},get sessionGeneration(){return B()},setPermissions(e){N=e.slice(),F+=1,P=`session-r${F}`},get telemetryEvents(){return m(oe)}},host:{artifact:{dependencies(e,t){return K(`artifact`,`dependencies`,t,()=>({value:[]}))},load(e,t){return K(`artifact`,`load`,t,()=>{let t=J(e.id);return{revision:t.revision,value:m(t.document)}})},publish(e,t){return q(`artifact`,`publish`,e,t,()=>de(e.id,t,`published`))},save(e,t){return q(`artifact`,`save`,e,t,()=>{ce(`studio.permission/save`);let n=J(e.id);return le(n,t),n.document=m(e),{revision:ue(n),value:null}})},unpublish(e,t){return q(`artifact`,`unpublish`,e,t,()=>de(e.id,t,`draft`))}},authoring:{listTypes(e,t){return K(`authoring`,`list-types`,t,()=>{Q(e.resourceContext.key,t),X(e.limit),Z(`list-types`,e);let n=e.search?.toLowerCase(),r=v.filter(r=>r.targetId===e.targetId&&r.resourceContextKey===t.resourceContextKey&&(n===void 0||r.definition.id.toLowerCase().includes(n)||(r.definition.label.defaultMessage?.toLowerCase().includes(n)??!1))).sort((e,t)=>h(e.definition.id,t.definition.id)||h(e.definition.version,t.definition.version)||h(e.definition.revision,t.definition.revision)),i=Y(e.cursor),a=i+e.limit;return{value:{items:r.slice(i,a).map(({definition:e})=>({blueprint:m(e.blueprint),label:m(e.label),model:m(e.model),reference:{id:e.id,revision:e.revision,version:e.version}})),...a<r.length?{nextCursor:p(a)}:{}}}})},planSave(e,t){return K(`authoring`,`plan-save`,t,()=>{let n=$(b,e,e=>e.intent);return Q(n.resourceContextKey,t),Z(`plan-save`,e),{value:m(n.plan)}})},resolveTarget(e,t){return K(`authoring`,`resolve-target`,t,()=>(Q(e.resourceContext.key,t),Z(`resolve-target`,e),{value:m($(_,e,e=>e.request).resolution)}))},saveAsNewType(e,t){return q(`authoring`,`save-as-new-type`,e,t,()=>{let n=$(x,e,e=>e.request);return n.request.kind!==`authoring-save-as-new-type-request`&&H(`not-found`,`The requested contextual save fixture is not available.`),Q(n.resourceContextKey,t),Z(`save-as-new-type`,e),{value:m(n.result)}})},saveItem(e,t){return q(`authoring`,`save-item`,e,t,()=>{let n=$(x,e,e=>e.request);return n.request.kind!==`authoring-save-item-request`&&H(`not-found`,`The requested contextual save fixture is not available.`),Q(n.resourceContextKey,t),Z(`save-item`,e),{value:m(n.result)}})},saveNewTypeVersion(e,t){return q(`authoring`,`save-new-type-version`,e,t,()=>{let n=$(x,e,e=>e.request);return n.request.kind!==`authoring-save-new-type-version-request`&&H(`not-found`,`The requested contextual save fixture is not available.`),Q(n.resourceContextKey,t),Z(`save-new-type-version`,e),{value:m(n.result)}})},start(e,t){return q(`authoring`,`start`,e,t,()=>(Q(e.resourceContext.key,t),Z(`start`,e),{value:m($(y,e,e=>e.request).session)}))}},localization:{messages(e,t,n){return K(`localization`,`messages`,n,()=>{let n=ae.get(e);n===void 0&&H(`not-found`,`The requested locale is not available.`);let r={};for(let[e,i]of Object.entries(n)){let n=e;t.some(e=>n===e||n.startsWith(`${e}.`))&&(r[n]=i)}return{value:r}})}},media:{abortUpload(e,t){return q(`media`,`abort-upload`,e,t,()=>(A.delete(e)||H(`not-found`,`The requested upload is not available.`),{value:null}))},authorizeUpload(e,t){return q(`media`,`authorize-upload`,e,t,()=>{ie(e),M+=1;let t=`uploads/testbed-${String(M)}`,n={expiresAt:`2030-01-01T00:00:00Z`,headers:{"X-Upload-Session":t},method:`PUT`,plan:{chunkBytes:5242880,maximumBytes:c,resumable:!0},uploadId:t,url:`https://uploads.testbed.invalid/${t}`};return A.set(t,{grant:n,request:m(e)}),{value:m(n)}})},completeUpload(e,t){return q(`media`,`complete-upload`,e,t,()=>{A.get(e)===void 0&&H(`not-found`,`The requested upload is not available.`),A.delete(e);let t={id:`assets/${e.replace(`uploads/`,``)}`,revision:`${e}-r1`,state:`processing`};return j.set(t.id,t),{revision:t.revision,value:m(t)}})},get(e,t){return K(`media`,`get`,t,()=>{let t=l.find(t=>t.id===e);return t===void 0?{value:null}:{revision:t.revision,value:m(t)}})},list(e,t){return K(`media`,`list`,t,()=>{X(e.limit);let t=e.search?.toLowerCase(),n=l.filter(n=>e.mediaTypes!==void 0&&!e.mediaTypes.includes(n.mediaType)?!1:t===void 0||n.filename.toLowerCase().includes(t)),r=Y(e.cursor),i=r+e.limit;return{value:{assets:m(n.slice(r,i)),...i<n.length?{nextCursor:p(i)}:{}}}})},importExternal(e,n){return q(`media`,`import-external`,e,n,()=>{let n=t(e);n.ok||H(`validation-failed`,`The external media candidate was refused: ${n.reason}.`),R+=1;let r={id:`assets/imported-${String(R)}`,revision:`imports/testbed-${String(R)}-r1`,state:`processing`};return j.set(r.id,r),{revision:r.revision,value:m(r)}})},uploadStatus(e,t){return K(`media`,`upload-status`,t,()=>{let t=j.get(e);return t===void 0&&H(`not-found`,`The requested asset is not available.`),{revision:t.revision,value:m(t)}})}},model:{get(e,t){return K(`model`,`get`,t,()=>{let t=J(e.id);return(t.document.kind!==`content-model`||t.document.version!==e.version||`revision`in e&&e.revision!==t.revision)&&H(`not-found`,`The requested model projection is not available.`),{revision:t.revision,value:m(t.document)}})},list(e){return K(`model`,`list`,e,()=>({value:[...s.values()].filter(e=>e.document.kind===`content-model`).map(e=>m(e.document)).sort((e,t)=>h(e.id,t.id)||h(e.version,t.version)||h(e.revision,t.revision))}))}},permission:{explain(e,t){return K(`permission`,`explain`,t,()=>({value:N.includes(e)?{allowed:!0}:{allowed:!1,reason:{defaultMessage:`The session does not hold this permission.`,key:`studio.testbed/permission-denied`}}}))},refresh(e){return K(`permission`,`refresh`,e,()=>({value:{permissions:N.slice(),sessionGeneration:B()}}))}},preview:{cancel(e,t){return K(`preview`,`cancel`,t,()=>(E.get(ee(e,t))?.cancel(),{value:null}))},render(e,t){return K(`preview`,`render`,t,async()=>{let n=ee(e.draftDigest,t);E.has(n)&&H(`invalid-request`,`A render for this draft is already in flight.`);let r=()=>void 0,i=new Promise((e,t)=>{r=()=>{t(new TestbedHostError(V(`cancelled`,`The in-flight preview render was cancelled.`,{retryable:!1})))}});E.set(n,{cancel:r});try{let t=await Promise.race([Promise.resolve(se(m(e))),i]);return(t.draftDigest!==e.draftDigest||t.requestId!==e.requestId)&&H(`internal`,`The preview renderer returned a result for a different render attempt.`),D.push(e.draftDigest),{value:m(t)}}finally{E.delete(n)}})}},recovery:{discard(e){return q(`recovery`,`discard`,null,e,()=>(C.delete(e.resourceContextKey),{value:null}))},load(e){return K(`recovery`,`load`,e,()=>{let t=C.get(e.resourceContextKey);return t===void 0?{value:null}:{value:m(t)}})},store(e,t){return q(`recovery`,`store`,e,t,()=>(C.set(t.resourceContextKey,m(e)),{value:null}))}},resource:{search(e,t){return K(`resource`,`search`,t,()=>{X(e.limit);let t=e.search?.toLowerCase(),n=f.filter(n=>n.resourceType===e.resourceType?t===void 0||(n.label.defaultMessage?.toLowerCase().includes(t)??!1):!1),r=Y(e.cursor),i=r+e.limit;return{value:{items:m(n.slice(r,i)),...i<n.length?{nextCursor:p(i)}:{}}}})}},telemetry:{emit(e,t){return q(`telemetry`,`emit`,e,t,()=>{if(e.attributes!==void 0)for(let t of Object.values(e.attributes))ne(t)||H(`invalid-request`,`Telemetry attribute values must be JSON primitives.`);return oe.push(m(e)),{value:null}})}}}}}function u(e){return{diagnostics:[],draftDigest:e.draftDigest,markerMap:{},markers:[],requestId:e.requestId}}function ee(e,t){return`${t.sessionGeneration}\u0000${t.resourceContextKey}\u0000${e}`}function te(e,t){let n=`${e}-r`;if(!t.startsWith(n))return 0;let r=t.slice(n.length),i=Number(r);return Number.isSafeInteger(i)&&i>=0&&String(i)===r?i:0}function d(e){return typeof e==`string`&&e.length>0}function ne(e){return e===null||typeof e==`boolean`||typeof e==`string`||typeof e==`number`&&Number.isFinite(e)}const f=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;function p(e){let t=`index:${e}`,n=``;for(let e=0;e<t.length;e+=3){let r=t.charCodeAt(e),i=e+1<t.length?t.charCodeAt(e+1):void 0,a=e+2<t.length?t.charCodeAt(e+2):void 0,o=r<<16|(i??0)<<8|(a??0);n+=f.charAt(o>>18&63),n+=f.charAt(o>>12&63),n+=i===void 0?`=`:f.charAt(o>>6&63),n+=a===void 0?`=`:f.charAt(o&63)}return n}function re(e){if(e.length===0||e.length%4!=0)return;let t=e.endsWith(`==`)?2:+!!e.endsWith(`=`),n=e.slice(0,e.length-t),r=0,i=0,a=``;for(let e of n){let t=f.indexOf(e);if(t<0)return;r=r<<6|t,i+=6,i>=8&&(i-=8,a+=String.fromCharCode(r>>i&255))}let o=/^index:(0|[1-9][0-9]*)$/u.exec(a)?.[1];if(!(o===void 0||o.length>9))return Number(o)}function m(e){return JSON.parse(JSON.stringify(e))}function h(e,t){return e<t?-1:+(e>t)}function g(t,n){let r=new Set;for(let i of n){let n=e(i);if(r.has(n))throw TypeError(`Testbed contextual authoring ${t} fixture is duplicated.`);r.add(n)}}function ie(e){if(e.byteSize>c)throw new TestbedHostError({category:`limit-exceeded`,contractVersion:r,kind:`host-error`,message:{defaultMessage:`The declared upload exceeds the accepted size.`,key:`studio.testbed/limit-exceeded`},retryable:!1});if(e.filename.includes(`/`)||e.filename.includes(`\\`))throw new TestbedHostError({category:`validation-failed`,contractVersion:r,kind:`host-error`,message:{defaultMessage:`The declared filename is not a canonical display name.`,key:`studio.testbed/validation-failed`},retryable:!1})}
@@ -0,0 +1,3 @@
1
+ /** Compatibility re-export of the canonical contextual HTTP route binding. */
2
+ export { AUTHORING_HTTP_OPERATIONS, AUTHORING_HTTP_OPERATIONS_BY_ROUTE, AUTHORING_HTTP_SCHEMA_ID, type AuthoringHttpOperationContract, type AuthoringHttpOperationRegistry, type AuthoringHttpRoute, type HttpSchemaValidator, } from '@kumwe/studio-core';
3
+ //# sourceMappingURL=http-authoring-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-authoring-contract.d.ts","sourceRoot":"","sources":["../src/http-authoring-contract.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,OAAO,EACL,yBAAyB,EACzB,kCAAkC,EAClC,wBAAwB,EACxB,KAAK,8BAA8B,EACnC,KAAK,8BAA8B,EACnC,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1 @@
1
+ export{AUTHORING_HTTP_OPERATIONS,AUTHORING_HTTP_OPERATIONS_BY_ROUTE,AUTHORING_HTTP_SCHEMA_ID}from"@kumwe/studio-core";