@kumwe/studio-core 0.1.0-alpha.10

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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/THIRD_PARTY_NOTICES.md +9 -0
  4. package/dist/binding-projection.d.ts +47 -0
  5. package/dist/binding-projection.d.ts.map +1 -0
  6. package/dist/binding-projection.js +253 -0
  7. package/dist/binding-projection.js.map +1 -0
  8. package/dist/canonical.d.ts +20 -0
  9. package/dist/canonical.d.ts.map +1 -0
  10. package/dist/canonical.js +100 -0
  11. package/dist/canonical.js.map +1 -0
  12. package/dist/clone.d.ts +2 -0
  13. package/dist/clone.d.ts.map +1 -0
  14. package/dist/clone.js +4 -0
  15. package/dist/clone.js.map +1 -0
  16. package/dist/commands.d.ts +18 -0
  17. package/dist/commands.d.ts.map +1 -0
  18. package/dist/commands.js +799 -0
  19. package/dist/commands.js.map +1 -0
  20. package/dist/contributions.d.ts +93 -0
  21. package/dist/contributions.d.ts.map +1 -0
  22. package/dist/contributions.js +435 -0
  23. package/dist/contributions.js.map +1 -0
  24. package/dist/entry-commands.d.ts +3 -0
  25. package/dist/entry-commands.d.ts.map +1 -0
  26. package/dist/entry-commands.js +36 -0
  27. package/dist/entry-commands.js.map +1 -0
  28. package/dist/extension-sdk.d.ts +51 -0
  29. package/dist/extension-sdk.d.ts.map +1 -0
  30. package/dist/extension-sdk.js +260 -0
  31. package/dist/extension-sdk.js.map +1 -0
  32. package/dist/history.d.ts +22 -0
  33. package/dist/history.d.ts.map +1 -0
  34. package/dist/history.js +81 -0
  35. package/dist/history.js.map +1 -0
  36. package/dist/host-session.d.ts +74 -0
  37. package/dist/host-session.d.ts.map +1 -0
  38. package/dist/host-session.js +713 -0
  39. package/dist/host-session.js.map +1 -0
  40. package/dist/index.d.ts +23 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +23 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/layout.d.ts +66 -0
  45. package/dist/layout.d.ts.map +1 -0
  46. package/dist/layout.js +240 -0
  47. package/dist/layout.js.map +1 -0
  48. package/dist/migrations.d.ts +47 -0
  49. package/dist/migrations.d.ts.map +1 -0
  50. package/dist/migrations.js +83 -0
  51. package/dist/migrations.js.map +1 -0
  52. package/dist/model-commands.d.ts +3 -0
  53. package/dist/model-commands.d.ts.map +1 -0
  54. package/dist/model-commands.js +21 -0
  55. package/dist/model-commands.js.map +1 -0
  56. package/dist/modes.d.ts +47 -0
  57. package/dist/modes.d.ts.map +1 -0
  58. package/dist/modes.js +280 -0
  59. package/dist/modes.js.map +1 -0
  60. package/dist/negotiation.d.ts +23 -0
  61. package/dist/negotiation.d.ts.map +1 -0
  62. package/dist/negotiation.js +65 -0
  63. package/dist/negotiation.js.map +1 -0
  64. package/dist/production-values.d.ts +12 -0
  65. package/dist/production-values.d.ts.map +1 -0
  66. package/dist/production-values.js +224 -0
  67. package/dist/production-values.js.map +1 -0
  68. package/dist/production.d.ts +86 -0
  69. package/dist/production.d.ts.map +1 -0
  70. package/dist/production.js +811 -0
  71. package/dist/production.js.map +1 -0
  72. package/dist/profile-validator.d.ts +47 -0
  73. package/dist/profile-validator.d.ts.map +1 -0
  74. package/dist/profile-validator.js +887 -0
  75. package/dist/profile-validator.js.map +1 -0
  76. package/dist/recipes.d.ts +15 -0
  77. package/dist/recipes.d.ts.map +1 -0
  78. package/dist/recipes.js +34 -0
  79. package/dist/recipes.js.map +1 -0
  80. package/dist/registry.d.ts +17 -0
  81. package/dist/registry.d.ts.map +1 -0
  82. package/dist/registry.js +56 -0
  83. package/dist/registry.js.map +1 -0
  84. package/dist/schema-profile.d.ts +27 -0
  85. package/dist/schema-profile.d.ts.map +1 -0
  86. package/dist/schema-profile.js +1015 -0
  87. package/dist/schema-profile.js.map +1 -0
  88. package/dist/semver.d.ts +20 -0
  89. package/dist/semver.d.ts.map +1 -0
  90. package/dist/semver.js +128 -0
  91. package/dist/semver.js.map +1 -0
  92. package/dist/session.d.ts +73 -0
  93. package/dist/session.d.ts.map +1 -0
  94. package/dist/session.js +191 -0
  95. package/dist/session.js.map +1 -0
  96. package/dist/url-policy.d.ts +45 -0
  97. package/dist/url-policy.d.ts.map +1 -0
  98. package/dist/url-policy.js +248 -0
  99. package/dist/url-policy.js.map +1 -0
  100. package/dist/validation.d.ts +13 -0
  101. package/dist/validation.d.ts.map +1 -0
  102. package/dist/validation.js +444 -0
  103. package/dist/validation.js.map +1 -0
  104. package/package.json +41 -0
@@ -0,0 +1,713 @@
1
+ import { HostPortFailure, STUDIO_CONTRACT_VERSION, STUDIO_STALE_SESSION_GENERATION_DIAGNOSTIC_CODE, commonSchema, contentModelSchema, isHostPortFailure, } from '@kumwe/studio-protocol';
2
+ import { canonicalStringify } from './canonical.js';
3
+ import { cloneContractValue } from './clone.js';
4
+ import { negotiateCapabilities } from './negotiation.js';
5
+ import { resolveSessionMode } from './modes.js';
6
+ import { compileProfileSchema } from './profile-validator.js';
7
+ import { StudioSession } from './session.js';
8
+ const ARTIFACT_PORT = 'studio.port/artifact';
9
+ const MODEL_PORT = 'studio.port/model';
10
+ const RECOVERY_PORT = 'studio.port/recovery';
11
+ const RESOURCE_PORT = 'studio.port/resource';
12
+ const ARTIFACT_LOAD = 'studio.operation/artifact.load';
13
+ const ARTIFACT_SAVE = 'studio.operation/artifact.save';
14
+ const MODEL_GET = 'studio.operation/model.get';
15
+ const MODEL_LIST = 'studio.operation/model.list';
16
+ const RECOVERY_STORE = 'studio.operation/recovery.store';
17
+ const RECOVERY_LOAD = 'studio.operation/recovery.load';
18
+ const RECOVERY_DISCARD = 'studio.operation/recovery.discard';
19
+ const RESOURCE_SEARCH = 'studio.operation/resource.search';
20
+ const STABLE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/u;
21
+ const QUALIFIED_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*\/[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u;
22
+ const FORBIDDEN_IDENTIFIERS = new Set(['__proto__', 'prototype', 'constructor']);
23
+ /** Public bounds enforced before a composed session invokes resource search. */
24
+ export const STUDIO_RESOURCE_SEARCH_LIMITS = Object.freeze({
25
+ maximumCursorLength: 500,
26
+ maximumLimit: 100,
27
+ maximumSearchLength: 500,
28
+ minimumLimit: 1,
29
+ });
30
+ // Model reads cross the same untrusted adapter boundary as artifact reads. The
31
+ // canonical schema is interpreted without eval before any model field reaches
32
+ // binding projection or an authoring shell.
33
+ const validateContentModelSchema = compileProfileSchema(contentModelSchema, { schemas: [commonSchema] });
34
+ const validateArtifactReferenceSchema = compileProfileSchema({
35
+ $ref: 'https://schemas.kumwe.org/studio/v1/common.schema.json#/$defs/artifactReference',
36
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
37
+ }, { schemas: [commonSchema] });
38
+ /** A local composition/state failure, distinct from a host-port rejection. */
39
+ export class StudioHostSessionError extends Error {
40
+ code;
41
+ diagnostics;
42
+ constructor(code, message, diagnostics = []) {
43
+ super(message);
44
+ this.name = 'StudioHostSessionError';
45
+ this.code = code;
46
+ this.diagnostics = cloneContractValue(diagnostics);
47
+ }
48
+ }
49
+ /**
50
+ * Composes the DOM-free editing engine with one resolved host configuration.
51
+ *
52
+ * The current contract-safe profile opens a single Blueprint only. It does
53
+ * not fabricate entry/model state, infer a transport, stage preview drafts,
54
+ * or reconcile recovery data. Those capabilities require their own explicit
55
+ * contracts.
56
+ */
57
+ export async function openStudioSession(adapter, options) {
58
+ const configuration = cloneContractValue(options.configuration);
59
+ const optionalPorts = requestedOptionalPorts(configuration, options.optionalPorts);
60
+ const negotiation = negotiateCapabilities(configuration.hostCapabilities, {
61
+ optionalPorts,
62
+ requiredPorts: [ARTIFACT_PORT],
63
+ supportedProtocolVersions: [configuration.protocolVersion],
64
+ });
65
+ if (configuration.sessionState === 'read-only') {
66
+ negotiation.sessionState = 'read-only';
67
+ }
68
+ appendProfileDiagnostics(configuration, negotiation);
69
+ const recoveryAvailable = appendOperationDiagnostics(adapter, configuration, negotiation);
70
+ const modelsAvailable = appendModelOperationDiagnostics(adapter, configuration, negotiation);
71
+ const resourcesAvailable = appendResourceOperationDiagnostics(adapter, configuration, negotiation);
72
+ if (negotiation.diagnostics.some((entry) => entry.severity === 'blocking')) {
73
+ throw new StudioHostSessionError('configuration-blocked', 'The resolved Studio configuration cannot open a Blueprint host session.', negotiation.diagnostics);
74
+ }
75
+ const reference = configuration.artifacts.blueprint;
76
+ if (reference === undefined) {
77
+ // `appendProfileDiagnostics` always made this path blocking. Keep the
78
+ // narrowing explicit so a future diagnostic refactor cannot fabricate a
79
+ // reference or issue a host call.
80
+ throw new StudioHostSessionError('configuration-blocked', 'A Blueprint host session requires an explicit locked Blueprint reference.', negotiation.diagnostics);
81
+ }
82
+ const identifiers = new SessionIdentifierAllocator(options.identifiers);
83
+ const loadContext = createContext(configuration, identifiers.requestId(ARTIFACT_LOAD), {
84
+ operationId: ARTIFACT_LOAD,
85
+ });
86
+ const loaded = await invokeOpeningHostCall(() => adapter.artifact.load(reference, loadContext));
87
+ if (!isBlueprintLoadResult(loaded, reference.id)) {
88
+ const diagnostic = createDiagnostic('studio.host/unexpected-artifact', 'The artifact port did not return the configured Blueprint.', 'blocking', { artifactId: reference.id });
89
+ throw new StudioHostSessionError('unexpected-artifact', 'The host returned an artifact outside the Blueprint session profile.', [diagnostic]);
90
+ }
91
+ const document = normalizeLoadedBlueprint(loaded.value, loaded.revision);
92
+ const session = new StudioSession({
93
+ document,
94
+ maximumHistoryEntries: configuration.limits.maxHistoryEntries,
95
+ mode: resolveSessionMode(configuration),
96
+ sessionGeneration: configuration.sessionGeneration,
97
+ });
98
+ session.markSaved(document.revision);
99
+ return new BoundStudioHostSession(adapter, configuration, identifiers, negotiation, recoveryAvailable, modelsAvailable, resourcesAvailable, session, document.revision);
100
+ }
101
+ class BoundStudioHostSession {
102
+ diagnostics;
103
+ negotiation;
104
+ models;
105
+ recovery;
106
+ resources;
107
+ session;
108
+ #adapter;
109
+ #configuration;
110
+ #identifiers;
111
+ #retryIntents = new Map();
112
+ #disposed = false;
113
+ #invalidationFailure;
114
+ #lastScheduledSave;
115
+ #revision;
116
+ #saveTail = Promise.resolve();
117
+ constructor(adapter, configuration, identifiers, negotiation, recoveryAvailable, modelsAvailable, resourcesAvailable, session, revision) {
118
+ this.#adapter = adapter;
119
+ this.#configuration = configuration;
120
+ this.#identifiers = identifiers;
121
+ this.negotiation = cloneNegotiation(negotiation);
122
+ this.diagnostics = cloneContractValue(negotiation.diagnostics);
123
+ this.session = session;
124
+ this.#revision = revision;
125
+ this.recovery = recoveryAvailable
126
+ ? Object.freeze({
127
+ discard: () => this.#discardRecovery(),
128
+ load: () => this.#loadRecovery(),
129
+ store: (envelope) => this.#storeRecovery(envelope),
130
+ })
131
+ : undefined;
132
+ this.models = modelsAvailable
133
+ ? Object.freeze({
134
+ get: (reference) => this.#getModel(reference),
135
+ list: () => this.#listModels(),
136
+ })
137
+ : undefined;
138
+ this.resources = resourcesAvailable
139
+ ? Object.freeze({
140
+ search: (query) => this.#searchResources(query),
141
+ })
142
+ : undefined;
143
+ }
144
+ get disposed() {
145
+ return this.#disposed;
146
+ }
147
+ get invalidated() {
148
+ return this.#invalidationFailure !== undefined;
149
+ }
150
+ get revision() {
151
+ return this.#revision;
152
+ }
153
+ dispose() {
154
+ if (this.#disposed) {
155
+ return;
156
+ }
157
+ this.#disposed = true;
158
+ this.#retryIntents.clear();
159
+ this.#identifiers.dispose();
160
+ }
161
+ save() {
162
+ try {
163
+ this.#assertActive();
164
+ if (this.session.mode === 'read-only') {
165
+ throw new StudioHostSessionError('read-only-session', 'A read-only Studio host session cannot save.');
166
+ }
167
+ const document = this.session.document;
168
+ const snapshotFingerprint = canonicalStringify(document);
169
+ const existing = this.#lastScheduledSave;
170
+ if (existing?.snapshotFingerprint === snapshotFingerprint) {
171
+ return existing.promise;
172
+ }
173
+ if (!this.session.dirty) {
174
+ return Promise.resolve({ revision: this.#revision, value: null });
175
+ }
176
+ const stateVersion = this.session.stateVersion;
177
+ const scheduled = this.#saveTail.then(() => this.#saveSnapshot(document, stateVersion));
178
+ this.#saveTail = scheduled.then(() => undefined, () => undefined);
179
+ this.#lastScheduledSave = { promise: scheduled, snapshotFingerprint };
180
+ const clear = () => {
181
+ if (this.#lastScheduledSave?.promise === scheduled) {
182
+ this.#lastScheduledSave = undefined;
183
+ }
184
+ };
185
+ void scheduled.then(clear, clear);
186
+ return scheduled;
187
+ }
188
+ catch (error) {
189
+ return Promise.reject(error instanceof Error
190
+ ? error
191
+ : new Error('The Studio host session failed with a non-Error rejection.'));
192
+ }
193
+ }
194
+ async #discardRecovery() {
195
+ this.#assertActive();
196
+ const recovery = this.#adapter.recovery;
197
+ if (recovery === undefined) {
198
+ throw adapterContractFailure('studio.host/adapter-port-unavailable', 'The negotiated recovery adapter is unavailable.');
199
+ }
200
+ const fingerprint = mutationFingerprint(null, this.#configuration);
201
+ const idempotencyKey = this.#mutationKey(RECOVERY_DISCARD, fingerprint);
202
+ const context = createContext(this.#configuration, this.#identifiers.requestId(RECOVERY_DISCARD), { idempotencyKey, operationId: RECOVERY_DISCARD });
203
+ const result = await this.#invoke(() => recovery.discard(context));
204
+ this.#clearMutationKey(RECOVERY_DISCARD, fingerprint);
205
+ return result;
206
+ }
207
+ async #loadRecovery() {
208
+ this.#assertActive();
209
+ const recovery = this.#adapter.recovery;
210
+ if (recovery === undefined) {
211
+ throw adapterContractFailure('studio.host/adapter-port-unavailable', 'The negotiated recovery adapter is unavailable.');
212
+ }
213
+ const context = createContext(this.#configuration, this.#identifiers.requestId(RECOVERY_LOAD), {
214
+ operationId: RECOVERY_LOAD,
215
+ });
216
+ const result = await this.#invoke(() => recovery.load(context));
217
+ return {
218
+ ...(result.revision === undefined ? {} : { revision: result.revision }),
219
+ value: result.value === null ? null : cloneContractValue(result.value),
220
+ };
221
+ }
222
+ async #getModel(reference) {
223
+ this.#assertActive();
224
+ if (!isArtifactReference(reference)) {
225
+ throw new StudioHostSessionError('invalid-model-reference', 'A model read requires a canonical artifact identifier and semantic version.');
226
+ }
227
+ const modelPort = this.#adapter.model;
228
+ if (modelPort === undefined) {
229
+ throw adapterContractFailure('studio.host/adapter-port-unavailable', 'The negotiated model adapter is unavailable.');
230
+ }
231
+ const referenceSnapshot = cloneContractValue(reference);
232
+ const context = createContext(this.#configuration, this.#identifiers.requestId(MODEL_GET), {
233
+ operationId: MODEL_GET,
234
+ });
235
+ const result = await this.#invoke(() => modelPort.get(referenceSnapshot, context));
236
+ if (!isModelGetResult(result, referenceSnapshot)) {
237
+ throw adapterContractFailure('studio.host/unexpected-model-result', 'The model port returned a document outside the requested model coordinate.');
238
+ }
239
+ return {
240
+ ...(result.revision === undefined ? {} : { revision: result.revision }),
241
+ value: cloneContractValue(result.value),
242
+ };
243
+ }
244
+ async #listModels() {
245
+ this.#assertActive();
246
+ const modelPort = this.#adapter.model;
247
+ if (modelPort === undefined) {
248
+ throw adapterContractFailure('studio.host/adapter-port-unavailable', 'The negotiated model adapter is unavailable.');
249
+ }
250
+ const context = createContext(this.#configuration, this.#identifiers.requestId(MODEL_LIST), {
251
+ operationId: MODEL_LIST,
252
+ });
253
+ const result = await this.#invoke(() => modelPort.list(context));
254
+ if (!isModelListResult(result)) {
255
+ throw adapterContractFailure('studio.host/unexpected-model-result', 'The model port returned a malformed or duplicate model collection.');
256
+ }
257
+ return {
258
+ ...(result.revision === undefined ? {} : { revision: result.revision }),
259
+ value: cloneContractValue(result.value).sort(compareModelCoordinates),
260
+ };
261
+ }
262
+ async #saveSnapshot(snapshot, stateVersion) {
263
+ this.#assertActive();
264
+ const expectedRevision = this.#revision;
265
+ const document = { ...snapshot, revision: expectedRevision };
266
+ const fingerprint = mutationFingerprint(document, this.#configuration, expectedRevision);
267
+ const idempotencyKey = this.#mutationKey(ARTIFACT_SAVE, fingerprint);
268
+ const context = createContext(this.#configuration, this.#identifiers.requestId(ARTIFACT_SAVE), {
269
+ expectedRevision,
270
+ idempotencyKey,
271
+ operationId: ARTIFACT_SAVE,
272
+ });
273
+ const result = await this.#invoke(() => this.#adapter.artifact.save(document, context));
274
+ if (result.value !== null || !isRevision(result.revision)) {
275
+ throw adapterContractFailure('studio.host/missing-accepted-revision', 'The artifact save did not return its accepted revision.');
276
+ }
277
+ this.#revision = result.revision;
278
+ this.session.markSaved(result.revision, stateVersion);
279
+ this.#clearMutationKey(ARTIFACT_SAVE, fingerprint);
280
+ return { revision: result.revision, value: null };
281
+ }
282
+ async #searchResources(query) {
283
+ this.#assertActive();
284
+ if (!isResourceSearchQuery(query)) {
285
+ throw new StudioHostSessionError('invalid-resource-query', 'A resource search requires a canonical resource type, bounded limit, cursor, and search text.');
286
+ }
287
+ const resource = this.#adapter.resource;
288
+ if (resource === undefined) {
289
+ throw adapterContractFailure('studio.host/adapter-port-unavailable', 'The negotiated resource adapter is unavailable.');
290
+ }
291
+ const querySnapshot = cloneContractValue(query);
292
+ const context = createContext(this.#configuration, this.#identifiers.requestId(RESOURCE_SEARCH), { operationId: RESOURCE_SEARCH });
293
+ const result = await this.#invoke(() => resource.search(querySnapshot, context));
294
+ if (!isResourceSearchResult(result, querySnapshot)) {
295
+ throw adapterContractFailure('studio.host/unexpected-resource-result', 'The resource port returned a malformed, mismatched, duplicate, or oversized search page.');
296
+ }
297
+ return {
298
+ ...(result.revision === undefined ? {} : { revision: result.revision }),
299
+ value: cloneContractValue(result.value),
300
+ };
301
+ }
302
+ async #storeRecovery(envelope) {
303
+ this.#assertActive();
304
+ const recovery = this.#adapter.recovery;
305
+ if (recovery === undefined) {
306
+ throw adapterContractFailure('studio.host/adapter-port-unavailable', 'The negotiated recovery adapter is unavailable.');
307
+ }
308
+ const value = cloneContractValue(envelope);
309
+ const fingerprint = mutationFingerprint(value, this.#configuration);
310
+ const idempotencyKey = this.#mutationKey(RECOVERY_STORE, fingerprint);
311
+ const context = createContext(this.#configuration, this.#identifiers.requestId(RECOVERY_STORE), { idempotencyKey, operationId: RECOVERY_STORE });
312
+ const result = await this.#invoke(() => recovery.store(value, context));
313
+ this.#clearMutationKey(RECOVERY_STORE, fingerprint);
314
+ return result;
315
+ }
316
+ #assertActive() {
317
+ if (this.#invalidationFailure !== undefined) {
318
+ throw this.#invalidationFailure;
319
+ }
320
+ if (this.#disposed) {
321
+ throw new StudioHostSessionError('disposed', 'The Studio host session is disposed.');
322
+ }
323
+ }
324
+ #clearMutationKey(operationId, fingerprint) {
325
+ if (this.#retryIntents.get(operationId)?.fingerprint === fingerprint) {
326
+ this.#retryIntents.delete(operationId);
327
+ }
328
+ }
329
+ async #invoke(operation) {
330
+ this.#assertActive();
331
+ try {
332
+ return await operation();
333
+ }
334
+ catch (error) {
335
+ const failure = normalizeHostRejection(error);
336
+ if (isStaleGenerationFailure(failure)) {
337
+ this.#invalidationFailure = failure;
338
+ }
339
+ throw failure;
340
+ }
341
+ }
342
+ #mutationKey(operationId, fingerprint) {
343
+ const prior = this.#retryIntents.get(operationId);
344
+ if (prior?.fingerprint === fingerprint) {
345
+ return prior.idempotencyKey;
346
+ }
347
+ const idempotencyKey = this.#identifiers.idempotencyKey(operationId, fingerprint);
348
+ this.#retryIntents.set(operationId, { fingerprint, idempotencyKey });
349
+ return idempotencyKey;
350
+ }
351
+ }
352
+ class SessionIdentifierAllocator {
353
+ #factories;
354
+ #idempotencyIntents = new Map();
355
+ #requestIds = new Set();
356
+ constructor(factories) {
357
+ this.#factories = factories;
358
+ }
359
+ dispose() {
360
+ this.#idempotencyIntents.clear();
361
+ this.#requestIds.clear();
362
+ }
363
+ idempotencyKey(operationId, fingerprint) {
364
+ const value = allocateIdentifier((candidateOperationId) => this.#factories.idempotencyKey(candidateOperationId), operationId, 'idempotency');
365
+ const scope = `${operationId}\u0000${value}`;
366
+ const prior = this.#idempotencyIntents.get(scope);
367
+ if (prior !== undefined && prior !== fingerprint) {
368
+ throw new StudioHostSessionError('invalid-identifier', 'The idempotency-key factory reused a key for another mutation intent.');
369
+ }
370
+ this.#idempotencyIntents.set(scope, fingerprint);
371
+ return value;
372
+ }
373
+ requestId(operationId) {
374
+ const value = allocateIdentifier((candidateOperationId) => this.#factories.requestId(candidateOperationId), operationId, 'request');
375
+ if (this.#requestIds.has(value)) {
376
+ throw new StudioHostSessionError('invalid-identifier', 'The request-ID factory returned an identifier already used by this session.');
377
+ }
378
+ this.#requestIds.add(value);
379
+ return value;
380
+ }
381
+ }
382
+ function allocateIdentifier(factory, operationId, purpose) {
383
+ let value;
384
+ try {
385
+ value = factory(operationId);
386
+ }
387
+ catch {
388
+ throw new StudioHostSessionError('invalid-identifier', `The ${purpose}-ID factory failed to allocate an identifier.`);
389
+ }
390
+ if (!isStableId(value)) {
391
+ throw new StudioHostSessionError('invalid-identifier', `The ${purpose}-ID factory returned a non-canonical stable identifier.`);
392
+ }
393
+ return value;
394
+ }
395
+ function appendOperationDiagnostics(adapter, configuration, negotiation) {
396
+ const artifact = configuration.hostCapabilities.ports.find((entry) => entry.id === ARTIFACT_PORT);
397
+ if (artifact !== undefined) {
398
+ const requiredOperations = [ARTIFACT_LOAD];
399
+ if (configuration.sessionState === 'editable') {
400
+ requiredOperations.push(ARTIFACT_SAVE);
401
+ }
402
+ for (const operationId of requiredOperations) {
403
+ if (!artifact.operations.includes(operationId)) {
404
+ negotiation.diagnostics.push(createDiagnostic('studio.host/missing-required-operation', `The host does not advertise the required ${operationId} operation.`, 'blocking', { operationId }));
405
+ }
406
+ }
407
+ }
408
+ if (!configuration.features.offlineRecovery) {
409
+ return false;
410
+ }
411
+ const recovery = configuration.hostCapabilities.ports.find((entry) => entry.id === RECOVERY_PORT);
412
+ if (recovery === undefined) {
413
+ return false;
414
+ }
415
+ let available = true;
416
+ for (const operationId of [RECOVERY_STORE, RECOVERY_LOAD, RECOVERY_DISCARD]) {
417
+ if (!recovery.operations.includes(operationId)) {
418
+ available = false;
419
+ negotiation.diagnostics.push(createDiagnostic('studio.host/missing-optional-operation', `The optional recovery port omits ${operationId}; recovery is disabled.`, 'information', { operationId }));
420
+ }
421
+ }
422
+ if (adapter.recovery === undefined) {
423
+ available = false;
424
+ negotiation.diagnostics.push(createDiagnostic('studio.host/adapter-port-unavailable', 'The capability document advertises recovery but the adapter does not implement it.', 'information', { port: RECOVERY_PORT }));
425
+ }
426
+ return available;
427
+ }
428
+ function appendModelOperationDiagnostics(adapter, configuration, negotiation) {
429
+ const model = configuration.hostCapabilities.ports.find((entry) => entry.id === MODEL_PORT);
430
+ if (model === undefined) {
431
+ return false;
432
+ }
433
+ let available = true;
434
+ for (const operationId of [MODEL_LIST, MODEL_GET]) {
435
+ if (!model.operations.includes(operationId)) {
436
+ available = false;
437
+ negotiation.diagnostics.push(createDiagnostic('studio.host/missing-optional-operation', `The model port omits ${operationId}; model binding is disabled.`, 'information', { operationId }));
438
+ }
439
+ }
440
+ if (adapter.model === undefined) {
441
+ available = false;
442
+ negotiation.diagnostics.push(createDiagnostic('studio.host/adapter-port-unavailable', 'The capability document advertises model reads but the adapter does not implement them.', 'information', { port: MODEL_PORT }));
443
+ }
444
+ return available;
445
+ }
446
+ function appendResourceOperationDiagnostics(adapter, configuration, negotiation) {
447
+ const resource = configuration.hostCapabilities.ports.find((entry) => entry.id === RESOURCE_PORT);
448
+ if (resource === undefined) {
449
+ return false;
450
+ }
451
+ let available = true;
452
+ if (!resource.operations.includes(RESOURCE_SEARCH)) {
453
+ available = false;
454
+ negotiation.diagnostics.push(createDiagnostic('studio.host/missing-optional-operation', `The resource port omits ${RESOURCE_SEARCH}; resource discovery is disabled.`, 'information', { operationId: RESOURCE_SEARCH }));
455
+ }
456
+ if (adapter.resource === undefined) {
457
+ available = false;
458
+ negotiation.diagnostics.push(createDiagnostic('studio.host/adapter-port-unavailable', 'The capability document advertises resource discovery but the adapter does not implement it.', 'information', { port: RESOURCE_PORT }));
459
+ }
460
+ return available;
461
+ }
462
+ function appendProfileDiagnostics(configuration, negotiation) {
463
+ if (configuration.artifacts.blueprint === undefined) {
464
+ negotiation.diagnostics.push(createDiagnostic('studio.host/missing-blueprint-artifact', 'A Blueprint session requires a locked Blueprint artifact reference.', 'blocking'));
465
+ }
466
+ if (configuration.mode !== 'blueprint' || configuration.composite !== 'single') {
467
+ negotiation.diagnostics.push(createDiagnostic('studio.host/unsupported-session-profile', 'This host-session profile opens only single Blueprint configurations.', 'blocking', { composite: configuration.composite, mode: configuration.mode }));
468
+ }
469
+ }
470
+ function requestedOptionalPorts(configuration, optionalPorts) {
471
+ const ports = new Set(optionalPorts ?? []);
472
+ ports.delete(ARTIFACT_PORT);
473
+ if (configuration.features.offlineRecovery) {
474
+ ports.add(RECOVERY_PORT);
475
+ }
476
+ if (configuration.hostCapabilities.ports.some((entry) => entry.id === MODEL_PORT)) {
477
+ ports.add(MODEL_PORT);
478
+ }
479
+ if (configuration.hostCapabilities.ports.some((entry) => entry.id === RESOURCE_PORT)) {
480
+ ports.add(RESOURCE_PORT);
481
+ }
482
+ return [...ports];
483
+ }
484
+ function createContext(configuration, requestId, options) {
485
+ return {
486
+ ...(options.expectedRevision === undefined
487
+ ? {}
488
+ : { expectedRevision: options.expectedRevision }),
489
+ ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
490
+ locale: configuration.locale.resolved,
491
+ operationId: options.operationId,
492
+ protocolVersion: configuration.protocolVersion,
493
+ requestId,
494
+ resourceContextKey: configuration.resourceContext.key,
495
+ sessionGeneration: configuration.sessionGeneration,
496
+ };
497
+ }
498
+ function createDiagnostic(code, defaultMessage, severity, parameters) {
499
+ return {
500
+ code,
501
+ message: { defaultMessage, key: code },
502
+ ...(parameters === undefined ? {} : { parameters }),
503
+ severity,
504
+ };
505
+ }
506
+ function normalizeLoadedBlueprint(document, resultRevision) {
507
+ const revision = resultRevision ?? document.revision;
508
+ if (!isRevision(revision)) {
509
+ throw new StudioHostSessionError('unexpected-artifact', 'The loaded Blueprint does not carry a valid accepted revision.', [
510
+ createDiagnostic('studio.host/missing-accepted-revision', 'The loaded Blueprint does not carry a valid accepted revision.', 'blocking'),
511
+ ]);
512
+ }
513
+ return cloneContractValue({ ...document, revision });
514
+ }
515
+ function isBlueprintLoadResult(value, artifactId) {
516
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || !('value' in value)) {
517
+ return false;
518
+ }
519
+ const document = value.value;
520
+ return (typeof document === 'object' &&
521
+ document !== null &&
522
+ !Array.isArray(document) &&
523
+ 'kind' in document &&
524
+ document.kind === 'blueprint' &&
525
+ 'id' in document &&
526
+ document.id === artifactId);
527
+ }
528
+ function isModelGetResult(value, reference) {
529
+ if (!isHostResultRecord(value) || !isContentModelDocument(value.value)) {
530
+ return false;
531
+ }
532
+ const expectedRevision = lockedReferenceRevision(reference);
533
+ return (value.value.id === reference.id &&
534
+ value.value.version === reference.version &&
535
+ (expectedRevision === undefined || value.value.revision === expectedRevision) &&
536
+ (value.revision === undefined || value.revision === value.value.revision));
537
+ }
538
+ function isModelListResult(value) {
539
+ if (!isHostResultRecord(value) || !Array.isArray(value.value)) {
540
+ return false;
541
+ }
542
+ const coordinates = new Set();
543
+ for (const model of value.value) {
544
+ if (!isContentModelDocument(model)) {
545
+ return false;
546
+ }
547
+ const coordinate = `${model.id}\u0000${model.version}\u0000${model.revision}`;
548
+ if (coordinates.has(coordinate)) {
549
+ return false;
550
+ }
551
+ coordinates.add(coordinate);
552
+ }
553
+ return true;
554
+ }
555
+ function isResourceSearchQuery(value) {
556
+ if (!isPlainRecord(value) ||
557
+ !hasExactKeys(value, ['limit', 'resourceType'], ['cursor', 'search'])) {
558
+ return false;
559
+ }
560
+ return (typeof value.limit === 'number' &&
561
+ Number.isSafeInteger(value.limit) &&
562
+ value.limit >= STUDIO_RESOURCE_SEARCH_LIMITS.minimumLimit &&
563
+ value.limit <= STUDIO_RESOURCE_SEARCH_LIMITS.maximumLimit &&
564
+ isQualifiedName(value.resourceType) &&
565
+ isBoundedOptionalString(value.cursor, STUDIO_RESOURCE_SEARCH_LIMITS.maximumCursorLength, false) &&
566
+ isBoundedOptionalString(value.search, STUDIO_RESOURCE_SEARCH_LIMITS.maximumSearchLength, true));
567
+ }
568
+ function isResourceSearchResult(value, query) {
569
+ if (!isHostResultRecord(value) || !isResourceSearchPage(value.value, query)) {
570
+ return false;
571
+ }
572
+ return true;
573
+ }
574
+ function isResourceSearchPage(value, query) {
575
+ if (!isPlainRecord(value) || !hasExactKeys(value, ['items'], ['nextCursor'])) {
576
+ return false;
577
+ }
578
+ if (!Array.isArray(value.items) ||
579
+ value.items.length > query.limit ||
580
+ !isBoundedOptionalString(value.nextCursor, STUDIO_RESOURCE_SEARCH_LIMITS.maximumCursorLength, false)) {
581
+ return false;
582
+ }
583
+ const identifiers = new Set();
584
+ for (const item of value.items) {
585
+ if (!isResourceSearchHit(item, query.resourceType) || identifiers.has(item.id)) {
586
+ return false;
587
+ }
588
+ identifiers.add(item.id);
589
+ }
590
+ return true;
591
+ }
592
+ function isResourceSearchHit(value, resourceType) {
593
+ return (isPlainRecord(value) &&
594
+ hasExactKeys(value, ['id', 'label', 'resourceType']) &&
595
+ isStableId(value.id) &&
596
+ value.resourceType === resourceType &&
597
+ isMessageReference(value.label));
598
+ }
599
+ function isMessageReference(value) {
600
+ return (isPlainRecord(value) &&
601
+ hasExactKeys(value, ['key'], ['defaultMessage']) &&
602
+ isQualifiedName(value.key) &&
603
+ (value.defaultMessage === undefined ||
604
+ (typeof value.defaultMessage === 'string' &&
605
+ value.defaultMessage.length >= 1 &&
606
+ value.defaultMessage.length <= 500)));
607
+ }
608
+ function isQualifiedName(value) {
609
+ return typeof value === 'string' && value.length <= 160 && QUALIFIED_NAME.test(value);
610
+ }
611
+ function isBoundedOptionalString(value, maximumLength, allowEmpty) {
612
+ return (value === undefined ||
613
+ (typeof value === 'string' &&
614
+ value.length <= maximumLength &&
615
+ (allowEmpty || value.length >= 1)));
616
+ }
617
+ function isPlainRecord(value) {
618
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
619
+ return false;
620
+ }
621
+ const prototype = Object.getPrototypeOf(value);
622
+ return prototype === Object.prototype || prototype === null;
623
+ }
624
+ function hasExactKeys(value, required, optional = []) {
625
+ const allowed = new Set([...required, ...optional]);
626
+ return (required.every((key) => Object.hasOwn(value, key)) &&
627
+ Object.keys(value).every((key) => allowed.has(key)));
628
+ }
629
+ function isHostResultRecord(value) {
630
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || !('value' in value)) {
631
+ return false;
632
+ }
633
+ return !('revision' in value) || isRevision(value.revision);
634
+ }
635
+ function isContentModelDocument(value) {
636
+ return validateContentModelSchema.validate(value);
637
+ }
638
+ function isArtifactReference(value) {
639
+ return validateArtifactReferenceSchema.validate(value);
640
+ }
641
+ function lockedReferenceRevision(reference) {
642
+ const candidate = reference;
643
+ return isRevision(candidate.revision) ? candidate.revision : undefined;
644
+ }
645
+ function compareModelCoordinates(left, right) {
646
+ return (compareCodeUnits(left.id, right.id) ||
647
+ compareCodeUnits(left.version, right.version) ||
648
+ compareCodeUnits(left.revision, right.revision));
649
+ }
650
+ function compareCodeUnits(left, right) {
651
+ return left < right ? -1 : left > right ? 1 : 0;
652
+ }
653
+ function mutationFingerprint(argument, configuration, expectedRevision) {
654
+ return canonicalStringify({
655
+ argument,
656
+ context: {
657
+ ...(expectedRevision === undefined ? {} : { expectedRevision }),
658
+ locale: configuration.locale.resolved,
659
+ protocolVersion: configuration.protocolVersion,
660
+ },
661
+ });
662
+ }
663
+ async function invokeOpeningHostCall(operation) {
664
+ try {
665
+ return await operation();
666
+ }
667
+ catch (error) {
668
+ throw normalizeHostRejection(error);
669
+ }
670
+ }
671
+ function normalizeHostRejection(error) {
672
+ if (isHostPortFailure(error)) {
673
+ return error;
674
+ }
675
+ return adapterContractFailure('studio.host/invalid-failure-wrapper', 'The host adapter rejected without a canonical HostPortFailure.');
676
+ }
677
+ function adapterContractFailure(code, defaultMessage) {
678
+ const error = {
679
+ category: 'internal',
680
+ contractVersion: STUDIO_CONTRACT_VERSION,
681
+ diagnostics: [createDiagnostic(code, defaultMessage, 'error')],
682
+ kind: 'host-error',
683
+ message: { defaultMessage, key: code },
684
+ retryable: false,
685
+ };
686
+ return new HostPortFailure(error);
687
+ }
688
+ function isStaleGenerationFailure(failure) {
689
+ return (failure.error.category === 'invalid-request' &&
690
+ (failure.error.diagnostics?.some((entry) => entry.code === STUDIO_STALE_SESSION_GENERATION_DIAGNOSTIC_CODE) ??
691
+ false));
692
+ }
693
+ function cloneNegotiation(value) {
694
+ return {
695
+ availablePorts: [...value.availablePorts],
696
+ diagnostics: cloneContractValue(value.diagnostics),
697
+ missingOptionalPorts: [...value.missingOptionalPorts],
698
+ missingRequiredPorts: [...value.missingRequiredPorts],
699
+ ...(value.protocolVersion === undefined ? {} : { protocolVersion: value.protocolVersion }),
700
+ sessionState: value.sessionState,
701
+ };
702
+ }
703
+ function isStableId(value) {
704
+ return (typeof value === 'string' &&
705
+ value.length >= 1 &&
706
+ value.length <= 240 &&
707
+ !FORBIDDEN_IDENTIFIERS.has(value) &&
708
+ STABLE_ID.test(value));
709
+ }
710
+ function isRevision(value) {
711
+ return typeof value === 'string' && value.length >= 1 && value.length <= 200;
712
+ }
713
+ //# sourceMappingURL=host-session.js.map