@superdoc/sdk 2.8.0 → 2.9.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,27 @@ await document.close();
25
25
  await client.dispose();
26
26
  ```
27
27
 
28
+ For token-authenticated Hocuspocus or y-websocket rooms, pass the credential as
29
+ an option on that document's open request:
30
+
31
+ ```js
32
+ const document = await client.open(
33
+ {
34
+ doc: './contract.docx',
35
+ collaboration: {
36
+ providerType: 'hocuspocus',
37
+ url: 'wss://collaboration.example.com',
38
+ documentId: 'contract-123',
39
+ },
40
+ },
41
+ { collaborationAuth: { type: 'token', token: userToken } },
42
+ );
43
+ ```
44
+
45
+ The credential applies only to that open request. It is not part of the
46
+ collaboration profile or command-line arguments. Per-open token authentication
47
+ is not supported for Liveblocks or the standalone document host.
48
+
28
49
  See the [Node.js SDK guide](https://docs.superdoc.dev/agents/automation/node-sdk) for a complete workflow.
29
50
 
30
51
  ## License
@@ -17,6 +17,7 @@ const CONTRACT = {
17
17
  "protocolVersion": "1.0",
18
18
  "features": [
19
19
  "cli.invoke",
20
+ "collaboration.auth.perOpen",
20
21
  "document.open",
21
22
  "document.open.defaultChangeMode",
22
23
  "document.currentRevision",
@@ -2901,6 +2901,7 @@ export const CONTRACT = {
2901
2901
  "transport": "stdio",
2902
2902
  "features": [
2903
2903
  "cli.invoke",
2904
+ "collaboration.auth.perOpen",
2904
2905
  "document.open",
2905
2906
  "document.open.defaultChangeMode",
2906
2907
  "document.currentRevision",
@@ -2931,6 +2932,7 @@ export const CONTRACT = {
2931
2932
  "protocolVersion": "1.0",
2932
2933
  "features": [
2933
2934
  "cli.invoke",
2935
+ "collaboration.auth.perOpen",
2934
2936
  "document.open",
2935
2937
  "document.open.defaultChangeMode",
2936
2938
  "document.currentRevision",
package/dist/index.cjs CHANGED
@@ -113,6 +113,7 @@ class SuperDocClient {
113
113
  rawApi;
114
114
  defaultDocumentRuntime;
115
115
  handles = new Map();
116
+ pendingSessionIds = new Set();
116
117
  constructor(options = {}) {
117
118
  this.defaultDocumentRuntime = options.runtime;
118
119
  this.runtime = new process.SuperDocRuntime(options);
@@ -133,18 +134,27 @@ class SuperDocClient {
133
134
  ? { ...params, runtime: this.defaultDocumentRuntime }
134
135
  : params;
135
136
  const explicitSessionId = openParams.sessionId;
136
- if (typeof explicitSessionId === 'string' && this.handles.has(explicitSessionId)) {
137
- throw new errors.SuperDocCliError(`Session id already open in this client: ${explicitSessionId}`, {
138
- code: 'SESSION_ALREADY_OPEN',
139
- details: { sessionId: explicitSessionId },
140
- });
137
+ if (typeof explicitSessionId === 'string') {
138
+ if (this.handles.has(explicitSessionId) || this.pendingSessionIds.has(explicitSessionId)) {
139
+ throw new errors.SuperDocCliError(`Session id already open in this client: ${explicitSessionId}`, {
140
+ code: 'SESSION_ALREADY_OPEN',
141
+ details: { sessionId: explicitSessionId },
142
+ });
143
+ }
144
+ this.pendingSessionIds.add(explicitSessionId);
145
+ }
146
+ try {
147
+ const result = (await this.rawApi.open(openParams, options));
148
+ const contextId = result.contextId;
149
+ const boundRuntime = new BoundRuntime(this.runtime, contextId);
150
+ const handle = new SuperDocDocument(boundRuntime, contextId, result, this);
151
+ this.handles.set(contextId, handle);
152
+ return handle;
153
+ }
154
+ finally {
155
+ if (typeof explicitSessionId === 'string')
156
+ this.pendingSessionIds.delete(explicitSessionId);
141
157
  }
142
- const result = (await this.rawApi.open(openParams, options));
143
- const contextId = result.contextId;
144
- const boundRuntime = new BoundRuntime(this.runtime, contextId);
145
- const handle = new SuperDocDocument(boundRuntime, contextId, result, this);
146
- this.handles.set(contextId, handle);
147
- return handle;
148
158
  }
149
159
  async describe(_params = {}, _options) {
150
160
  return introspection.describeContract();
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type BoundDocApi, type DocCloseBoundParams, type DocCloseResult, type DocFormatApplyBoundParams, type DocFormatApplyResult, type DocOpenParams as GeneratedDocOpenParams, type DocOpenResult, type DocSaveBoundParams, type DocSaveResult } from './generated/client.js';
2
- import { SuperDocRuntime, type SuperDocClientOptions, type InvokeOptions, type OperationSpec, type RuntimeInvoker } from './runtime/process.js';
2
+ import { SuperDocRuntime, type SuperDocClientOptions, type DocOpenOptions, type InvokeOptions, type OperationSpec, type RuntimeInvoker } from './runtime/process.js';
3
3
  /**
4
4
  * Wraps a raw runtime and injects a fixed sessionId into every invoke call.
5
5
  * Implements RuntimeInvoker so generated code can use it directly.
@@ -64,6 +64,7 @@ export declare class SuperDocClient {
64
64
  private readonly rawApi;
65
65
  private readonly defaultDocumentRuntime;
66
66
  private readonly handles;
67
+ private readonly pendingSessionIds;
67
68
  constructor(options?: SuperDocClientOptions);
68
69
  connect(): Promise<void>;
69
70
  /**
@@ -73,7 +74,7 @@ export declare class SuperDocClient {
73
74
  * automatically. The same file can be opened multiple times with
74
75
  * different session ids (useful for diff workflows).
75
76
  */
76
- open(params: DocOpenParams, options?: InvokeOptions): Promise<SuperDocDocument>;
77
+ open(params: DocOpenParams, options?: DocOpenOptions): Promise<SuperDocDocument>;
77
78
  describe(_params?: Record<string, unknown>, _options?: InvokeOptions): Promise<unknown>;
78
79
  describeCommand(params: DocDescribeCommandParams, _options?: InvokeOptions): Promise<unknown>;
79
80
  dispose(): Promise<void>;
@@ -93,5 +94,5 @@ export type { GetSystemPromptOptions, GetToolsOptions, GetToolsResult, PresetDes
93
94
  export { createAgentToolkit } from './tools.js';
94
95
  export type { AgentToolkit, CreateAgentToolkitInput } from './tools.js';
95
96
  export { SuperDocCliError } from './runtime/errors.js';
96
- export type { InvokeOptions, OperationSpec, OperationParamSpec, RuntimeInvoker, SuperDocClientOptions, } from './runtime/process.js';
97
+ export type { CollaborationAuth, DocOpenOptions, InvokeOptions, OperationSpec, OperationParamSpec, RuntimeInvoker, SuperDocClientOptions, } from './runtime/process.js';
97
98
  export type { DocOpenResult } from './generated/client.js';
package/dist/index.js CHANGED
@@ -105,6 +105,7 @@ export class SuperDocClient {
105
105
  rawApi;
106
106
  defaultDocumentRuntime;
107
107
  handles = new Map();
108
+ pendingSessionIds = new Set();
108
109
  constructor(options = {}) {
109
110
  this.defaultDocumentRuntime = options.runtime;
110
111
  this.runtime = new SuperDocRuntime(options);
@@ -125,18 +126,27 @@ export class SuperDocClient {
125
126
  ? { ...params, runtime: this.defaultDocumentRuntime }
126
127
  : params;
127
128
  const explicitSessionId = openParams.sessionId;
128
- if (typeof explicitSessionId === 'string' && this.handles.has(explicitSessionId)) {
129
- throw new SuperDocCliError(`Session id already open in this client: ${explicitSessionId}`, {
130
- code: 'SESSION_ALREADY_OPEN',
131
- details: { sessionId: explicitSessionId },
132
- });
129
+ if (typeof explicitSessionId === 'string') {
130
+ if (this.handles.has(explicitSessionId) || this.pendingSessionIds.has(explicitSessionId)) {
131
+ throw new SuperDocCliError(`Session id already open in this client: ${explicitSessionId}`, {
132
+ code: 'SESSION_ALREADY_OPEN',
133
+ details: { sessionId: explicitSessionId },
134
+ });
135
+ }
136
+ this.pendingSessionIds.add(explicitSessionId);
137
+ }
138
+ try {
139
+ const result = (await this.rawApi.open(openParams, options));
140
+ const contextId = result.contextId;
141
+ const boundRuntime = new BoundRuntime(this.runtime, contextId);
142
+ const handle = new SuperDocDocument(boundRuntime, contextId, result, this);
143
+ this.handles.set(contextId, handle);
144
+ return handle;
145
+ }
146
+ finally {
147
+ if (typeof explicitSessionId === 'string')
148
+ this.pendingSessionIds.delete(explicitSessionId);
133
149
  }
134
- const result = (await this.rawApi.open(openParams, options));
135
- const contextId = result.contextId;
136
- const boundRuntime = new BoundRuntime(this.runtime, contextId);
137
- const handle = new SuperDocDocument(boundRuntime, contextId, result, this);
138
- this.handles.set(contextId, handle);
139
- return handle;
140
150
  }
141
151
  async describe(_params = {}, _options) {
142
152
  return describeContract();
@@ -392,26 +392,78 @@ async function coreGetCatalog() {
392
392
  };
393
393
  }
394
394
  /**
395
- * Drop the per-action documentation lines for excluded actions. Entries render
396
- * as single "- name: ..." lines (the drift-guard test enforces the format), so
397
- * line-level filtering is deterministic. Prose cross-references elsewhere in
398
- * the prompt are left alone a mention costs a few tokens; a full per-action
399
- * manual for an uncallable action teaches the model to call it.
395
+ * Sentence boundary: terminal punctuation, whitespace, then something that
396
+ * plausibly opens a sentence including lowercase, because prompt sentences
397
+ * regularly start with an action name ("comment_paragraphs applies ...").
398
+ * The lookbehind guards keep "e.g."/"i.e." from splitting mid-sentence; a
399
+ * false split is still safe because kept fragments are rejoined with their
400
+ * original whitespace, and erring toward MORE splits only narrows what a
401
+ * mention takes down with it.
402
+ */
403
+ const SENTENCE_BOUNDARY = /((?<![ei]\.[ge]\.)(?<=[.!?])\s+(?=[A-Za-z"(]))/;
404
+ function mentionsAnyAction(text, excludedActions) {
405
+ for (const name of excludedActions) {
406
+ if (new RegExp(`\\b${name}\\b`).test(text))
407
+ return true;
408
+ }
409
+ return false;
410
+ }
411
+ /**
412
+ * Drop the sentences of one prompt line that mention an excluded action.
413
+ * Returns null when nothing usable is left, so the caller drops the line.
414
+ */
415
+ function stripExcludedActionSentences(line, excludedActions) {
416
+ if (!mentionsAnyAction(line, excludedActions))
417
+ return line;
418
+ // split() with a capturing group keeps the inter-sentence whitespace as
419
+ // separate array elements: even indexes are sentences, odd their separators.
420
+ const parts = line.split(SENTENCE_BOUNDARY);
421
+ const kept = [];
422
+ for (let i = 0; i < parts.length; i += 2) {
423
+ if (mentionsAnyAction(parts[i], excludedActions))
424
+ continue;
425
+ kept.push(parts[i]);
426
+ if (i + 1 < parts.length)
427
+ kept.push(parts[i + 1]);
428
+ }
429
+ let result = kept.join('').trimEnd();
430
+ // Losing the first sentence must not lose the bullet marker with it.
431
+ if (result && /^- /.test(line) && !/^- /.test(result))
432
+ result = `- ${result}`;
433
+ // Nothing left beyond the marker means the whole line was about excluded
434
+ // actions — drop it entirely rather than leaving an empty bullet.
435
+ return /[a-zA-Z0-9]/.test(result.replace(/^- /, '')) ? result : null;
436
+ }
437
+ /**
438
+ * Remove excluded actions from the prompt: drop their per-action
439
+ * documentation lines (entries render as single "- name: ..." lines — the
440
+ * drift-guard test enforces the format), then drop any remaining sentence
441
+ * that still mentions one. Guidance prose ("use move_range to relocate a
442
+ * section") is instruction, not a passing cross-reference: leaving it coaches
443
+ * the model into calls the schema forbids and dispatch rejects.
400
444
  */
401
445
  function stripExcludedActionLines(prompt, excludedActions) {
402
446
  if (excludedActions.size === 0)
403
447
  return prompt;
404
448
  return prompt
405
449
  .split('\n')
406
- .filter((line) => {
450
+ .map((line) => {
407
451
  const match = /^- ([a-z_]+)(?: \/ ([a-z_]+))?:/.exec(line);
408
452
  if (!match)
409
- return true;
453
+ return line;
410
454
  const names = [match[1], match[2]].filter((n) => Boolean(n));
411
- // Drop the line only when EVERY action it documents is excluded (the
412
- // paired accept/reject line survives if one side remains callable).
413
- return !names.every((name) => excludedActions.has(name));
455
+ const callable = names.filter((name) => !excludedActions.has(name));
456
+ // Drop the line only when EVERY action it documents is excluded; a
457
+ // paired accept/reject line survives if one side remains callable, but
458
+ // its header keeps only the callable name.
459
+ if (callable.length === 0)
460
+ return null;
461
+ if (callable.length === names.length)
462
+ return line;
463
+ return `- ${callable.join(' / ')}:${line.slice(match[0].length)}`;
414
464
  })
465
+ .map((line) => (line === null ? null : stripExcludedActionSentences(line, excludedActions)))
466
+ .filter((line) => line !== null)
415
467
  .join('\n');
416
468
  }
417
469
  async function coreGetSystemPrompt(options) {
@@ -388,26 +388,78 @@ async function coreGetCatalog() {
388
388
  };
389
389
  }
390
390
  /**
391
- * Drop the per-action documentation lines for excluded actions. Entries render
392
- * as single "- name: ..." lines (the drift-guard test enforces the format), so
393
- * line-level filtering is deterministic. Prose cross-references elsewhere in
394
- * the prompt are left alone a mention costs a few tokens; a full per-action
395
- * manual for an uncallable action teaches the model to call it.
391
+ * Sentence boundary: terminal punctuation, whitespace, then something that
392
+ * plausibly opens a sentence including lowercase, because prompt sentences
393
+ * regularly start with an action name ("comment_paragraphs applies ...").
394
+ * The lookbehind guards keep "e.g."/"i.e." from splitting mid-sentence; a
395
+ * false split is still safe because kept fragments are rejoined with their
396
+ * original whitespace, and erring toward MORE splits only narrows what a
397
+ * mention takes down with it.
398
+ */
399
+ const SENTENCE_BOUNDARY = /((?<![ei]\.[ge]\.)(?<=[.!?])\s+(?=[A-Za-z"(]))/;
400
+ function mentionsAnyAction(text, excludedActions) {
401
+ for (const name of excludedActions) {
402
+ if (new RegExp(`\\b${name}\\b`).test(text))
403
+ return true;
404
+ }
405
+ return false;
406
+ }
407
+ /**
408
+ * Drop the sentences of one prompt line that mention an excluded action.
409
+ * Returns null when nothing usable is left, so the caller drops the line.
410
+ */
411
+ function stripExcludedActionSentences(line, excludedActions) {
412
+ if (!mentionsAnyAction(line, excludedActions))
413
+ return line;
414
+ // split() with a capturing group keeps the inter-sentence whitespace as
415
+ // separate array elements: even indexes are sentences, odd their separators.
416
+ const parts = line.split(SENTENCE_BOUNDARY);
417
+ const kept = [];
418
+ for (let i = 0; i < parts.length; i += 2) {
419
+ if (mentionsAnyAction(parts[i], excludedActions))
420
+ continue;
421
+ kept.push(parts[i]);
422
+ if (i + 1 < parts.length)
423
+ kept.push(parts[i + 1]);
424
+ }
425
+ let result = kept.join('').trimEnd();
426
+ // Losing the first sentence must not lose the bullet marker with it.
427
+ if (result && /^- /.test(line) && !/^- /.test(result))
428
+ result = `- ${result}`;
429
+ // Nothing left beyond the marker means the whole line was about excluded
430
+ // actions — drop it entirely rather than leaving an empty bullet.
431
+ return /[a-zA-Z0-9]/.test(result.replace(/^- /, '')) ? result : null;
432
+ }
433
+ /**
434
+ * Remove excluded actions from the prompt: drop their per-action
435
+ * documentation lines (entries render as single "- name: ..." lines — the
436
+ * drift-guard test enforces the format), then drop any remaining sentence
437
+ * that still mentions one. Guidance prose ("use move_range to relocate a
438
+ * section") is instruction, not a passing cross-reference: leaving it coaches
439
+ * the model into calls the schema forbids and dispatch rejects.
396
440
  */
397
441
  function stripExcludedActionLines(prompt, excludedActions) {
398
442
  if (excludedActions.size === 0)
399
443
  return prompt;
400
444
  return prompt
401
445
  .split('\n')
402
- .filter((line) => {
446
+ .map((line) => {
403
447
  const match = /^- ([a-z_]+)(?: \/ ([a-z_]+))?:/.exec(line);
404
448
  if (!match)
405
- return true;
449
+ return line;
406
450
  const names = [match[1], match[2]].filter((n) => Boolean(n));
407
- // Drop the line only when EVERY action it documents is excluded (the
408
- // paired accept/reject line survives if one side remains callable).
409
- return !names.every((name) => excludedActions.has(name));
451
+ const callable = names.filter((name) => !excludedActions.has(name));
452
+ // Drop the line only when EVERY action it documents is excluded; a
453
+ // paired accept/reject line survives if one side remains callable, but
454
+ // its header keeps only the callable name.
455
+ if (callable.length === 0)
456
+ return null;
457
+ if (callable.length === names.length)
458
+ return line;
459
+ return `- ${callable.join(' / ')}:${line.slice(match[0].length)}`;
410
460
  })
461
+ .map((line) => (line === null ? null : stripExcludedActionSentences(line, excludedActions)))
462
+ .filter((line) => line !== null)
411
463
  .join('\n');
412
464
  }
413
465
  async function coreGetSystemPrompt(options) {
@@ -8,6 +8,7 @@ var documentRpc = require('./document-rpc.cjs');
8
8
 
9
9
  const HOST_PROTOCOL_VERSION = '1.0';
10
10
  const CLI_HOST_REQUIRED_FEATURES = ['cli.invoke', 'host.shutdown'];
11
+ const COLLABORATION_AUTH_PER_OPEN_FEATURE = 'collaboration.auth.perOpen';
11
12
  const DOCUMENT_HOST_REQUIRED_FEATURES = [...documentRpc.DOCUMENT_RPC_FEATURES, 'host.shutdown'];
12
13
  const CHANGE_MODES = ['direct', 'tracked'];
13
14
  const FORWARD_HOST_STDERR = typeof process !== 'undefined' && typeof process.env?.SUPERDOC_DEBUG_TEXT_REWRITE === 'string'
@@ -187,7 +188,27 @@ class HostTransport {
187
188
  return this.runWhileActive(() => this.invokeWhileActive(operation, params, options));
188
189
  }
189
190
  async invokeWhileActive(operation, params, options) {
191
+ const collaborationAuth = options.collaborationAuth === undefined ? undefined : transportCommon.normalizeCollaborationAuth(options.collaborationAuth);
192
+ if (collaborationAuth !== undefined) {
193
+ if (operation.operationId !== 'doc.open') {
194
+ throw new errors.SuperDocCliError('collaborationAuth is supported only for doc.open requests.', {
195
+ code: 'INVALID_ARGUMENT',
196
+ });
197
+ }
198
+ if (this.processMode !== 'cli') {
199
+ throw new errors.SuperDocCliError('Per-open collaboration authentication is not supported by the standalone document host.', {
200
+ code: 'CAPABILITY_UNSUPPORTED',
201
+ details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
202
+ });
203
+ }
204
+ }
190
205
  await this.ensureConnected();
206
+ if (collaborationAuth !== undefined && !this.hostFeatures.has(COLLABORATION_AUTH_PER_OPEN_FEATURE)) {
207
+ throw new errors.SuperDocCliError('The connected CLI host does not support per-open collaboration authentication.', {
208
+ code: 'CAPABILITY_UNSUPPORTED',
209
+ details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
210
+ });
211
+ }
191
212
  const shouldOpenThroughCliTimeoutFallback = this.processMode === 'cli' &&
192
213
  (this.requestTimeoutMs !== undefined ||
193
214
  (options.timeoutMs !== undefined && !this.hostFeatures.has(documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE)));
@@ -282,6 +303,7 @@ class HostTransport {
282
303
  const response = await this.sendJsonRpcRequest('cli.invoke', {
283
304
  argv,
284
305
  stdinBase64,
306
+ ...(collaborationAuth === undefined ? {} : { collaborationAuth }),
285
307
  }, watchdogTimeout);
286
308
  if (typeof response !== 'object' || response == null || Array.isArray(response)) {
287
309
  throw new errors.SuperDocCliError('Host returned invalid cli.invoke result.', {
@@ -669,6 +691,7 @@ class HostTransport {
669
691
  }
670
692
  }
671
693
 
694
+ exports.COLLABORATION_AUTH_PER_OPEN_FEATURE = COLLABORATION_AUTH_PER_OPEN_FEATURE;
672
695
  exports.HostTransport = HostTransport;
673
696
  exports.buildDocumentHostSpawnArgs = buildDocumentHostSpawnArgs;
674
697
  exports.buildHostSpawnArgs = buildHostSpawnArgs;
@@ -1,4 +1,5 @@
1
- import { type InvokeOptions, type OperationSpec, type SuperDocClientOptions } from './transport-common.js';
1
+ import { type OperationSpec, type SuperDocClientOptions, type TransportInvokeOptions } from './transport-common.js';
2
+ export declare const COLLABORATION_AUTH_PER_OPEN_FEATURE = "collaboration.auth.perOpen";
2
3
  export declare function mapCliInvocationResult(operation: OperationSpec, value: unknown): unknown;
3
4
  /**
4
5
  * Builds the argv passed to `spawn` for `superdoc host --stdio`. Propagates
@@ -62,7 +63,7 @@ export declare class HostTransport {
62
63
  connect(): Promise<void>;
63
64
  dispose(): Promise<void>;
64
65
  private disposeAfterActiveCalls;
65
- invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: InvokeOptions): Promise<TData>;
66
+ invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: TransportInvokeOptions): Promise<TData>;
66
67
  private invokeWhileActive;
67
68
  private runWhileActive;
68
69
  private ensureConnected;
@@ -1,10 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { buildOperationArgv, resolveInvocation, } from './transport-common.js';
3
+ import { buildOperationArgv, normalizeCollaborationAuth, resolveInvocation, } from './transport-common.js';
4
4
  import { SuperDocCliError } from './errors.js';
5
5
  import { DOCUMENT_RPC_FEATURES, DOCUMENT_RPC_DEFAULT_CHANGE_MODE_FEATURE, DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE, DOCUMENT_RPC_SOURCE_SAVE_FEATURE, buildDocumentInvokeParams, buildDocumentOpenParams, canOpenWithDocumentRpc, documentRpcRequestSupportsResponseTimeout, documentRpcEnabled, mapDocumentLifecycleResult, mapDocumentOpenResult, supportsDocumentRpc, } from './document-rpc.js';
6
6
  const HOST_PROTOCOL_VERSION = '1.0';
7
7
  const CLI_HOST_REQUIRED_FEATURES = ['cli.invoke', 'host.shutdown'];
8
+ export const COLLABORATION_AUTH_PER_OPEN_FEATURE = 'collaboration.auth.perOpen';
8
9
  const DOCUMENT_HOST_REQUIRED_FEATURES = [...DOCUMENT_RPC_FEATURES, 'host.shutdown'];
9
10
  const CHANGE_MODES = ['direct', 'tracked'];
10
11
  const FORWARD_HOST_STDERR = typeof process !== 'undefined' && typeof process.env?.SUPERDOC_DEBUG_TEXT_REWRITE === 'string'
@@ -184,7 +185,27 @@ export class HostTransport {
184
185
  return this.runWhileActive(() => this.invokeWhileActive(operation, params, options));
185
186
  }
186
187
  async invokeWhileActive(operation, params, options) {
188
+ const collaborationAuth = options.collaborationAuth === undefined ? undefined : normalizeCollaborationAuth(options.collaborationAuth);
189
+ if (collaborationAuth !== undefined) {
190
+ if (operation.operationId !== 'doc.open') {
191
+ throw new SuperDocCliError('collaborationAuth is supported only for doc.open requests.', {
192
+ code: 'INVALID_ARGUMENT',
193
+ });
194
+ }
195
+ if (this.processMode !== 'cli') {
196
+ throw new SuperDocCliError('Per-open collaboration authentication is not supported by the standalone document host.', {
197
+ code: 'CAPABILITY_UNSUPPORTED',
198
+ details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
199
+ });
200
+ }
201
+ }
187
202
  await this.ensureConnected();
203
+ if (collaborationAuth !== undefined && !this.hostFeatures.has(COLLABORATION_AUTH_PER_OPEN_FEATURE)) {
204
+ throw new SuperDocCliError('The connected CLI host does not support per-open collaboration authentication.', {
205
+ code: 'CAPABILITY_UNSUPPORTED',
206
+ details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
207
+ });
208
+ }
188
209
  const shouldOpenThroughCliTimeoutFallback = this.processMode === 'cli' &&
189
210
  (this.requestTimeoutMs !== undefined ||
190
211
  (options.timeoutMs !== undefined && !this.hostFeatures.has(DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE)));
@@ -279,6 +300,7 @@ export class HostTransport {
279
300
  const response = await this.sendJsonRpcRequest('cli.invoke', {
280
301
  argv,
281
302
  stdinBase64,
303
+ ...(collaborationAuth === undefined ? {} : { collaborationAuth }),
282
304
  }, watchdogTimeout);
283
305
  if (typeof response !== 'object' || response == null || Array.isArray(response)) {
284
306
  throw new SuperDocCliError('Host returned invalid cli.invoke result.', {
@@ -32,6 +32,10 @@ function resolveRuntimeProcess(options = {}, embedded = {
32
32
  processMode: 'cli',
33
33
  };
34
34
  }
35
+ function toSafeInvokeTraceOptions(options) {
36
+ const { collaborationAuth: _collaborationAuth, ...traceOptions } = options;
37
+ return traceOptions;
38
+ }
35
39
  /**
36
40
  * Internal runtime that delegates operations to a persistent host transport.
37
41
  *
@@ -60,7 +64,7 @@ class SuperDocRuntime {
60
64
  phase: 'start',
61
65
  operationId: operation.operationId,
62
66
  params,
63
- options,
67
+ options: toSafeInvokeTraceOptions(options),
64
68
  });
65
69
  try {
66
70
  const result = await this.transport.invoke(operation, params, options);
@@ -88,3 +92,4 @@ class SuperDocRuntime {
88
92
 
89
93
  exports.SuperDocRuntime = SuperDocRuntime;
90
94
  exports.resolveRuntimeProcess = resolveRuntimeProcess;
95
+ exports.toSafeInvokeTraceOptions = toSafeInvokeTraceOptions;
@@ -1,4 +1,4 @@
1
- import type { DocumentRuntimeKind, InvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions } from './transport-common.js';
1
+ import type { CollaborationAuth, DocumentRuntimeKind, DocOpenOptions, InvokeOptions, TransportInvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions } from './transport-common.js';
2
2
  type RuntimeProcessResolution = {
3
3
  hostBin: string;
4
4
  processMode: 'cli' | 'document';
@@ -8,6 +8,7 @@ type EmbeddedRuntimeResolvers = {
8
8
  documentHost: () => string;
9
9
  };
10
10
  export declare function resolveRuntimeProcess(options?: SuperDocClientOptions, embedded?: EmbeddedRuntimeResolvers): RuntimeProcessResolution;
11
+ export declare function toSafeInvokeTraceOptions(options: TransportInvokeOptions): InvokeOptions;
11
12
  /**
12
13
  * Internal runtime that delegates operations to a persistent host transport.
13
14
  *
@@ -19,6 +20,6 @@ export declare class SuperDocRuntime {
19
20
  constructor(options?: SuperDocClientOptions);
20
21
  connect(): Promise<void>;
21
22
  dispose(): Promise<void>;
22
- invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: InvokeOptions): Promise<TData>;
23
+ invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: TransportInvokeOptions): Promise<TData>;
23
24
  }
24
- export type { DocumentRuntimeKind, InvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions, };
25
+ export type { CollaborationAuth, DocumentRuntimeKind, DocOpenOptions, InvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions, };
@@ -29,6 +29,10 @@ export function resolveRuntimeProcess(options = {}, embedded = {
29
29
  processMode: 'cli',
30
30
  };
31
31
  }
32
+ export function toSafeInvokeTraceOptions(options) {
33
+ const { collaborationAuth: _collaborationAuth, ...traceOptions } = options;
34
+ return traceOptions;
35
+ }
32
36
  /**
33
37
  * Internal runtime that delegates operations to a persistent host transport.
34
38
  *
@@ -57,7 +61,7 @@ export class SuperDocRuntime {
57
61
  phase: 'start',
58
62
  operationId: operation.operationId,
59
63
  params,
60
- options,
64
+ options: toSafeInvokeTraceOptions(options),
61
65
  });
62
66
  try {
63
67
  const result = await this.transport.invoke(operation, params, options);
@@ -3,6 +3,6 @@
3
3
  // AUTO-GENERATED by scripts/embed-version.mjs — DO NOT EDIT.
4
4
  // Source of truth: package.json. Regenerated on every SDK build so the
5
5
  // SDK retains its own version identity when bundled into another package.
6
- const SDK_VERSION = '2.8.0';
6
+ const SDK_VERSION = '2.9.0-next.1';
7
7
 
8
8
  exports.SDK_VERSION = SDK_VERSION;
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.8.0";
1
+ export declare const SDK_VERSION = "2.9.0-next.1";
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/embed-version.mjs — DO NOT EDIT.
2
2
  // Source of truth: package.json. Regenerated on every SDK build so the
3
3
  // SDK retains its own version identity when bundled into another package.
4
- export const SDK_VERSION = '2.8.0';
4
+ export const SDK_VERSION = '2.9.0-next.1';
@@ -2,6 +2,27 @@
2
2
 
3
3
  var errors = require('./errors.cjs');
4
4
 
5
+ function normalizeCollaborationAuth(value) {
6
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
7
+ throw new errors.SuperDocCliError('collaborationAuth must be an object.', { code: 'INVALID_ARGUMENT' });
8
+ }
9
+ const record = value;
10
+ const unknownField = Object.keys(record).find((key) => key !== 'type' && key !== 'token');
11
+ if (unknownField) {
12
+ throw new errors.SuperDocCliError(`collaborationAuth.${unknownField} is not supported.`, {
13
+ code: 'INVALID_ARGUMENT',
14
+ });
15
+ }
16
+ if (record.type !== 'token') {
17
+ throw new errors.SuperDocCliError('collaborationAuth.type must be "token".', { code: 'INVALID_ARGUMENT' });
18
+ }
19
+ if (typeof record.token !== 'string' || record.token.length === 0) {
20
+ throw new errors.SuperDocCliError('collaborationAuth.token must be a non-empty string.', {
21
+ code: 'INVALID_ARGUMENT',
22
+ });
23
+ }
24
+ return { type: 'token', token: record.token };
25
+ }
5
26
  function hasExtension(filePath, extension) {
6
27
  return filePath.toLowerCase().endsWith(extension);
7
28
  }
@@ -165,4 +186,5 @@ function buildOperationArgv(operation, params, options, runtimeTimeoutMs, defaul
165
186
 
166
187
  exports.applyOperationParamAliases = applyOperationParamAliases;
167
188
  exports.buildOperationArgv = buildOperationArgv;
189
+ exports.normalizeCollaborationAuth = normalizeCollaborationAuth;
168
190
  exports.resolveInvocation = resolveInvocation;
@@ -19,6 +19,17 @@ export interface InvokeOptions {
19
19
  expectedRevision?: string | number;
20
20
  changeMode?: ChangeMode;
21
21
  }
22
+ export interface CollaborationAuth {
23
+ type: 'token';
24
+ token: string;
25
+ }
26
+ export interface DocOpenOptions extends InvokeOptions {
27
+ collaborationAuth?: CollaborationAuth;
28
+ }
29
+ export interface TransportInvokeOptions extends InvokeOptions {
30
+ collaborationAuth?: CollaborationAuth;
31
+ }
32
+ export declare function normalizeCollaborationAuth(value: unknown): CollaborationAuth;
22
33
  /**
23
34
  * Minimal invoke interface that both SuperDocRuntime and BoundRuntime satisfy.
24
35
  * Generated code depends on this interface, not on the concrete runtime class.
@@ -1,4 +1,25 @@
1
1
  import { SuperDocCliError } from './errors.js';
2
+ export function normalizeCollaborationAuth(value) {
3
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
4
+ throw new SuperDocCliError('collaborationAuth must be an object.', { code: 'INVALID_ARGUMENT' });
5
+ }
6
+ const record = value;
7
+ const unknownField = Object.keys(record).find((key) => key !== 'type' && key !== 'token');
8
+ if (unknownField) {
9
+ throw new SuperDocCliError(`collaborationAuth.${unknownField} is not supported.`, {
10
+ code: 'INVALID_ARGUMENT',
11
+ });
12
+ }
13
+ if (record.type !== 'token') {
14
+ throw new SuperDocCliError('collaborationAuth.type must be "token".', { code: 'INVALID_ARGUMENT' });
15
+ }
16
+ if (typeof record.token !== 'string' || record.token.length === 0) {
17
+ throw new SuperDocCliError('collaborationAuth.token must be a non-empty string.', {
18
+ code: 'INVALID_ARGUMENT',
19
+ });
20
+ }
21
+ return { type: 'token', token: record.token };
22
+ }
2
23
  function hasExtension(filePath, extension) {
3
24
  return filePath.toLowerCase().endsWith(extension);
4
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superdoc/sdk",
3
- "version": "2.8.0",
3
+ "version": "2.9.0-next.1",
4
4
  "description": "Node SDK for SuperDoc, wrapping the SuperDoc CLI to read and edit .docx files from JavaScript and TypeScript.",
5
5
  "private": false,
6
6
  "license": "AGPL-3.0",
@@ -37,19 +37,20 @@
37
37
  "typescript": "^5.9.2"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@superdoc/sdk-darwin-arm64": "2.8.0",
41
- "@superdoc/sdk-darwin-x64": "2.8.0",
42
- "@superdoc/sdk-linux-x64": "2.8.0",
43
- "@superdoc/sdk-linux-arm64": "2.8.0",
44
- "@superdoc/sdk-windows-x64": "2.8.0"
40
+ "@superdoc/sdk-darwin-arm64": "2.9.0-next.1",
41
+ "@superdoc/sdk-darwin-x64": "2.9.0-next.1",
42
+ "@superdoc/sdk-linux-x64": "2.9.0-next.1",
43
+ "@superdoc/sdk-linux-arm64": "2.9.0-next.1",
44
+ "@superdoc/sdk-windows-x64": "2.9.0-next.1"
45
45
  },
46
46
  "publishConfig": {
47
47
  "access": "public"
48
48
  },
49
49
  "scripts": {
50
- "build": "rm -rf dist && node scripts/embed-version.mjs && node scripts/embed-prompts.mjs && node scripts/embed-tools.mjs && tsc && rollup -c rollup.cjs.config.mjs && rm -rf dist/prompts && mkdir -p dist/prompts && cp src/prompts/*.md dist/prompts/ && pnpm run audit:publish",
50
+ "build": "rm -rf dist && node scripts/embed-version.mjs && node scripts/embed-prompts.mjs && node scripts/embed-tools.mjs && tsc && pnpm run typecheck:consumer && rollup -c rollup.cjs.config.mjs && rm -rf dist/prompts && mkdir -p dist/prompts && cp src/prompts/*.md dist/prompts/ && pnpm run audit:publish",
51
51
  "audit:publish": "node ../../../../scripts/audit-publish-artifact.mjs dist --label sdk-node-dist",
52
52
  "typecheck": "tsc --noEmit",
53
+ "typecheck:consumer": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext ../../../../tests/consumer-typecheck/src/sdk-per-open-collaboration-auth.mts",
53
54
  "test:document-host": "bun test src/runtime/__tests__/host-spawn-args.test.ts src/runtime/__tests__/document-rpc.test.ts src/__tests__/structured-document-rpc.e2e.test.ts src/__tests__/request-timeout-ms.e2e.test.ts",
54
55
  "smoke:product-action": "node scripts/product-action-smoke.mjs"
55
56
  }