@canonmsg/agent-sdk 5.0.0 → 5.1.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.
@@ -53,6 +53,15 @@ export declare class CanonAgent {
53
53
  private approvalManager;
54
54
  private approvalManagerAgentId;
55
55
  private approvalManagerOwnerId;
56
+ /**
57
+ * Shared poll/timeout/abort engine for interactive runtime input + card
58
+ * requests. The server request is still created inline (host-specific
59
+ * `native`/`responseUserId`), so the descriptors register a no-op `create`
60
+ * but keep the built-in `cancel`: passing the turn's abort `signal` lets the
61
+ * manager fire `consume({ cancel: true })` on interrupt, so the hosts no
62
+ * longer hand-roll a `cancelPendingRequest`.
63
+ */
64
+ private runtimeRequestManager;
56
65
  private cachedConversationIds;
57
66
  private running;
58
67
  private runtimeHeartbeatTimer;
@@ -66,6 +75,16 @@ export declare class CanonAgent {
66
75
  private sseConnectedLogged;
67
76
  constructor(options: CanonAgentOptions);
68
77
  private ensureApprovalManager;
78
+ /**
79
+ * Shared engine for interactive runtime input + card polling. Unlike approval,
80
+ * these families do not need an owner (the request is created inline with the
81
+ * caller's own `responseUserId` policy), so the manager is always available.
82
+ * The registered descriptors keep the built-in poll/consume/timeout logic and
83
+ * the built-in `cancel` (best-effort `consume({ cancel: true })`), replacing
84
+ * only `create` with a no-op because the request is already created inline.
85
+ * On abort the manager runs that cancel via the passed `signal`.
86
+ */
87
+ private ensureRuntimeRequestManager;
69
88
  private filterApprovalReplyMessages;
70
89
  on(event: 'message', handler: MessageHandler): void;
71
90
  on(event: 'messageUpdated', handler: MessageUpdatedHandler): void;
@@ -1,4 +1,4 @@
1
- import { ApprovalManager, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, sendMessageWithRetry, } from '@canonmsg/core';
1
+ import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, sendMessageWithRetry, } from '@canonmsg/core';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
@@ -10,7 +10,6 @@ const RUNTIME_CONTROL_POLL_INTERVAL_MS = 2_000;
10
10
  const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
11
11
  const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
12
12
  const DEFAULT_RUNTIME_INPUT_TIMEOUT_MS = 5 * 60_000;
13
- const RUNTIME_INPUT_POLL_MS = 1_000;
14
13
  const RUNTIME_INPUT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,160}$/;
15
14
  const SDK_MESSAGE_ID_READABLE_MAX = 120;
16
15
  const SDK_RUNTIME_CAPABILITIES = {
@@ -168,6 +167,9 @@ function safeRuntimeCardId(value) {
168
167
  ? normalized
169
168
  : `card_${randomUUID()}`;
170
169
  }
170
+ function normalizeResponseUserId(value) {
171
+ return value?.trim() || undefined;
172
+ }
171
173
  function buildSdkMessageId(parts) {
172
174
  const raw = parts
173
175
  .map((part) => part == null ? '' : String(part))
@@ -268,6 +270,15 @@ export class CanonAgent {
268
270
  approvalManager = null;
269
271
  approvalManagerAgentId = null;
270
272
  approvalManagerOwnerId = null;
273
+ /**
274
+ * Shared poll/timeout/abort engine for interactive runtime input + card
275
+ * requests. The server request is still created inline (host-specific
276
+ * `native`/`responseUserId`), so the descriptors register a no-op `create`
277
+ * but keep the built-in `cancel`: passing the turn's abort `signal` lets the
278
+ * manager fire `consume({ cancel: true })` on interrupt, so the hosts no
279
+ * longer hand-roll a `cancelPendingRequest`.
280
+ */
281
+ runtimeRequestManager = null;
271
282
  cachedConversationIds = [];
272
283
  running = false;
273
284
  runtimeHeartbeatTimer = null;
@@ -354,6 +365,33 @@ export class CanonAgent {
354
365
  this.approvalManagerOwnerId = ownerId;
355
366
  return this.approvalManager;
356
367
  }
368
+ /**
369
+ * Shared engine for interactive runtime input + card polling. Unlike approval,
370
+ * these families do not need an owner (the request is created inline with the
371
+ * caller's own `responseUserId` policy), so the manager is always available.
372
+ * The registered descriptors keep the built-in poll/consume/timeout logic and
373
+ * the built-in `cancel` (best-effort `consume({ cancel: true })`), replacing
374
+ * only `create` with a no-op because the request is already created inline.
375
+ * On abort the manager runs that cancel via the passed `signal`.
376
+ */
377
+ ensureRuntimeRequestManager() {
378
+ if (!this.runtimeRequestManager) {
379
+ const manager = new RuntimeRequestManager(this.apiClient, {
380
+ agentId: this.agentId ?? '',
381
+ ownerId: '',
382
+ });
383
+ manager.register('input', {
384
+ ...runtimeInputDescriptor,
385
+ create: async ({ requestId }) => ({ requestId }),
386
+ });
387
+ manager.register('card', {
388
+ ...runtimeCardDescriptor,
389
+ create: async ({ requestId }) => ({ requestId }),
390
+ });
391
+ this.runtimeRequestManager = manager;
392
+ }
393
+ return this.runtimeRequestManager;
394
+ }
357
395
  filterApprovalReplyMessages(conversationId, messages) {
358
396
  const manager = this.ensureApprovalManager();
359
397
  if (!manager) {
@@ -1275,6 +1313,9 @@ export class CanonAgent {
1275
1313
  }
1276
1314
  }
1277
1315
  const latestMessage = hydratedMessages[hydratedMessages.length - 1] ?? null;
1316
+ const triggeringHumanId = latestMessage?.senderType === 'human'
1317
+ ? normalizeResponseUserId(latestMessage.senderId)
1318
+ : undefined;
1278
1319
  let replyContext = latestMessage
1279
1320
  ? resolveCanonReplyContext({ message: latestMessage, messages: history })
1280
1321
  : null;
@@ -1438,6 +1479,11 @@ export class CanonAgent {
1438
1479
  await this.typingSignals.clear(conversationId);
1439
1480
  }
1440
1481
  catch { }
1482
+ const responseUserId = normalizeResponseUserId(request.responseUserId)
1483
+ ?? triggeringHumanId;
1484
+ // Session-wide rules are owner-scoped. The effective responder, not
1485
+ // merely the member who triggered the turn, determines that scope.
1486
+ const mustIsolateSessionRules = Boolean(responseUserId && responseUserId !== agent.ownerId);
1441
1487
  const result = await manager.requestApproval(conversationId, request.toolName, request.toolInput ?? {}, {
1442
1488
  riskLevel: request.riskLevel,
1443
1489
  risk: request.risk,
@@ -1447,8 +1493,9 @@ export class CanonAgent {
1447
1493
  native: request.native,
1448
1494
  toolSummary: request.toolSummary,
1449
1495
  details: request.details,
1450
- ignoreSessionRules: request.ignoreSessionRules,
1451
- allowSessionRule: request.allowSessionRule,
1496
+ ...(responseUserId ? { responseUserId } : {}),
1497
+ ignoreSessionRules: mustIsolateSessionRules ? true : request.ignoreSessionRules,
1498
+ allowSessionRule: mustIsolateSessionRules ? false : request.allowSessionRule,
1452
1499
  });
1453
1500
  throwIfAborted();
1454
1501
  shouldPersistTurnState = false;
@@ -1481,20 +1528,15 @@ export class CanonAgent {
1481
1528
  const expiresAt = new Date(expiresAtMs).toISOString();
1482
1529
  let result = { status: 'timeout', inputId };
1483
1530
  let requestCreated = false;
1484
- let requestResolved = false;
1485
- const cancelPendingRequest = async () => {
1486
- if (!requestCreated || requestResolved)
1487
- return;
1488
- try {
1489
- await this.apiClient.consumeRuntimeInputResponse({
1490
- conversationId,
1491
- inputId,
1492
- cancel: true,
1493
- });
1494
- requestResolved = true;
1495
- }
1496
- catch { }
1497
- };
1531
+ const ownerOnly = request.kind === 'secret'
1532
+ || request.kind === 'sudo'
1533
+ || request.sensitive === true
1534
+ || Boolean(request.questions?.some((question) => question.isSecret));
1535
+ const responseUserId = ownerOnly
1536
+ ? normalizeResponseUserId(agent.ownerId)
1537
+ : normalizeResponseUserId(request.responseUserId)
1538
+ ?? triggeringHumanId
1539
+ ?? normalizeResponseUserId(agent.ownerId);
1498
1540
  shouldPersistTurnState = true;
1499
1541
  try {
1500
1542
  await this.apiClient.createRuntimeInputRequest({
@@ -1502,7 +1544,7 @@ export class CanonAgent {
1502
1544
  inputId,
1503
1545
  kind: request.kind,
1504
1546
  expiresAt: expiresAtMs,
1505
- responseUserId: agent.ownerId,
1547
+ ...(responseUserId ? { responseUserId } : {}),
1506
1548
  title: request.title,
1507
1549
  prompt: request.prompt,
1508
1550
  ...(request.choices ? { choices: request.choices } : {}),
@@ -1529,56 +1571,15 @@ export class CanonAgent {
1529
1571
  await this.typingSignals.clear(conversationId);
1530
1572
  }
1531
1573
  catch { }
1532
- while (Date.now() < expiresAtMs) {
1533
- throwIfAborted();
1534
- try {
1535
- const response = await this.apiClient.consumeRuntimeInputResponse({
1536
- conversationId,
1537
- inputId,
1538
- });
1539
- if (response.status === 'submitted') {
1540
- requestResolved = true;
1541
- result = {
1542
- status: 'submitted',
1543
- value: response.value,
1544
- answers: response.answers,
1545
- inputId,
1546
- };
1547
- break;
1548
- }
1549
- if (response.status === 'cancelled' || response.status === 'timeout') {
1550
- requestResolved = true;
1551
- result = { status: response.status, inputId };
1552
- break;
1553
- }
1554
- }
1555
- catch {
1556
- // Transient consume failures should not leak sensitive input or
1557
- // break the runtime; keep waiting until the explicit timeout.
1558
- }
1559
- await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
1560
- }
1561
- if (!requestResolved) {
1562
- try {
1563
- const response = await this.apiClient.consumeRuntimeInputResponse({
1564
- conversationId,
1565
- inputId,
1566
- });
1567
- if (response.status === 'submitted') {
1568
- result = {
1569
- status: 'submitted',
1570
- value: response.value,
1571
- answers: response.answers,
1572
- inputId,
1573
- };
1574
- }
1575
- else if (response.status === 'cancelled' || response.status === 'timeout') {
1576
- result = { status: response.status, inputId };
1577
- }
1578
- }
1579
- catch { }
1580
- requestResolved = true;
1581
- }
1574
+ // The shared engine owns the poll-with-abort loop + post-deadline
1575
+ // consume; the request was already created above, so the descriptor's
1576
+ // create is a no-op. On abort the manager fires the descriptor's
1577
+ // built-in cancel (consume({ cancel: true })) via the passed signal.
1578
+ result = await this.ensureRuntimeRequestManager().request('input', conversationId, { kind: request.kind }, {
1579
+ requestId: inputId,
1580
+ expiresAt: expiresAtMs,
1581
+ signal: abortController.signal,
1582
+ });
1582
1583
  const outcome = buildRuntimeInputOutcome(inputId, result.status, {
1583
1584
  kind: request.kind,
1584
1585
  reason: result.status,
@@ -1607,8 +1608,16 @@ export class CanonAgent {
1607
1608
  return result;
1608
1609
  }
1609
1610
  catch (error) {
1610
- await cancelPendingRequest();
1611
1611
  if (abortController.signal.aborted || isAbortLikeError(error)) {
1612
+ if (requestCreated) {
1613
+ // Abort landing before the manager wired its cancel (e.g. during
1614
+ // the pre-request writeTurn/typing round-trips) leaves the
1615
+ // server-side request pending + responder-actionable; cancel it so
1616
+ // a late (possibly sensitive) answer can't resolve a dead request.
1617
+ await this.apiClient
1618
+ .consumeRuntimeInputResponse({ conversationId, inputId, cancel: true })
1619
+ .catch(() => { });
1620
+ }
1612
1621
  throw error;
1613
1622
  }
1614
1623
  if (!requestCreated) {
@@ -1633,9 +1642,20 @@ export class CanonAgent {
1633
1642
  const expiresAtMs = explicitExpiresAt && Number.isFinite(explicitExpiresAt)
1634
1643
  ? explicitExpiresAt
1635
1644
  : Date.now() + timeoutMs;
1645
+ const responseUserId = normalizeResponseUserId(request.responseUserId)
1646
+ ?? triggeringHumanId;
1647
+ const routedRequest = responseUserId
1648
+ ? { ...request, responseUserId }
1649
+ : request;
1636
1650
  // Fire-and-forget: post the durable card and return. The backend treats an
1637
1651
  // action-less card as display (no pending state, no response expected).
1638
- await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({ request, conversationId, cardId, fallbackTurnId: turnId, expiresAtMs }));
1652
+ await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({
1653
+ request: routedRequest,
1654
+ conversationId,
1655
+ cardId,
1656
+ fallbackTurnId: turnId,
1657
+ expiresAtMs,
1658
+ }));
1639
1659
  try {
1640
1660
  await turnOutput.addBlock({
1641
1661
  id: `card:${cardId}`,
@@ -1667,25 +1687,23 @@ export class CanonAgent {
1667
1687
  const expiresAtMs = explicitExpiresAt && Number.isFinite(explicitExpiresAt)
1668
1688
  ? explicitExpiresAt
1669
1689
  : Date.now() + timeoutMs;
1690
+ const responseUserId = normalizeResponseUserId(request.responseUserId)
1691
+ ?? triggeringHumanId;
1692
+ const routedRequest = responseUserId
1693
+ ? { ...request, responseUserId }
1694
+ : request;
1670
1695
  let result = { status: 'timeout', cardId };
1671
1696
  let requestCreated = false;
1672
1697
  let requestResolved = false;
1673
- const cancelPendingRequest = async () => {
1674
- if (!requestCreated || requestResolved)
1675
- return;
1676
- try {
1677
- await this.apiClient.consumeRuntimeCardResponse({
1678
- conversationId,
1679
- cardId,
1680
- cancel: true,
1681
- });
1682
- requestResolved = true;
1683
- }
1684
- catch { }
1685
- };
1686
1698
  shouldPersistTurnState = true;
1687
1699
  try {
1688
- await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({ request, conversationId, cardId, fallbackTurnId: turnId, expiresAtMs }));
1700
+ await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({
1701
+ request: routedRequest,
1702
+ conversationId,
1703
+ cardId,
1704
+ fallbackTurnId: turnId,
1705
+ expiresAtMs,
1706
+ }));
1689
1707
  requestCreated = true;
1690
1708
  try {
1691
1709
  await turnOutput.addBlock({
@@ -1703,56 +1721,16 @@ export class CanonAgent {
1703
1721
  await this.typingSignals.clear(conversationId);
1704
1722
  }
1705
1723
  catch { }
1706
- while (Date.now() < expiresAtMs) {
1707
- throwIfAborted();
1708
- try {
1709
- const response = await this.apiClient.consumeRuntimeCardResponse({
1710
- conversationId,
1711
- cardId,
1712
- });
1713
- if (response.status === 'submitted') {
1714
- requestResolved = true;
1715
- result = {
1716
- status: 'submitted',
1717
- cardId,
1718
- ...(response.actionId ? { actionId: response.actionId } : {}),
1719
- ...(response.values ? { values: response.values } : {}),
1720
- };
1721
- break;
1722
- }
1723
- if (response.status === 'cancelled' || response.status === 'timeout') {
1724
- requestResolved = true;
1725
- result = { status: response.status, cardId };
1726
- break;
1727
- }
1728
- }
1729
- catch {
1730
- // Keep waiting; the control path is the authoritative source and
1731
- // transient consume failures should not leak response values.
1732
- }
1733
- await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
1734
- }
1735
- if (!requestResolved) {
1736
- try {
1737
- const response = await this.apiClient.consumeRuntimeCardResponse({
1738
- conversationId,
1739
- cardId,
1740
- });
1741
- if (response.status === 'submitted') {
1742
- result = {
1743
- status: 'submitted',
1744
- cardId,
1745
- ...(response.actionId ? { actionId: response.actionId } : {}),
1746
- ...(response.values ? { values: response.values } : {}),
1747
- };
1748
- }
1749
- else if (response.status === 'cancelled' || response.status === 'timeout') {
1750
- result = { status: response.status, cardId };
1751
- }
1752
- }
1753
- catch { }
1754
- requestResolved = true;
1755
- }
1724
+ // The shared engine owns the poll-with-abort loop + post-deadline
1725
+ // consume; the request was already created above, so the descriptor's
1726
+ // create is a no-op. On abort the manager fires the descriptor's
1727
+ // built-in cancel (consume({ cancel: true })) via the passed signal.
1728
+ result = await this.ensureRuntimeRequestManager().request('card', conversationId, { card: request.card }, {
1729
+ requestId: cardId,
1730
+ expiresAt: expiresAtMs,
1731
+ signal: abortController.signal,
1732
+ });
1733
+ requestResolved = true;
1756
1734
  // The interactive path never yields 'displayed' (that returns early via
1757
1735
  // sendCard); narrow for buildRuntimeCardOutcome's resolution status.
1758
1736
  const resolutionStatus = result.status === 'displayed' ? 'timeout' : result.status;
@@ -1784,9 +1762,14 @@ export class CanonAgent {
1784
1762
  }
1785
1763
  catch (error) {
1786
1764
  const shouldSendInterruptedOutcome = requestCreated && !requestResolved;
1787
- await cancelPendingRequest();
1788
1765
  if (abortController.signal.aborted || isAbortLikeError(error)) {
1789
1766
  if (shouldSendInterruptedOutcome) {
1767
+ // Cancel the server-side card if the abort landed before the
1768
+ // manager wired its cancel, so a late action can't resolve a dead
1769
+ // request (mirrors the runtime-input path).
1770
+ await this.apiClient
1771
+ .consumeRuntimeCardResponse({ conversationId, cardId, cancel: true })
1772
+ .catch(() => { });
1790
1773
  const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
1791
1774
  await sendDurableMessage(outcome.text, {
1792
1775
  metadata: {
package/dist/types.d.ts CHANGED
@@ -59,6 +59,8 @@ export interface RuntimeApprovalRequest {
59
59
  turnId?: string;
60
60
  native?: ApprovalNativeRequestMetadata;
61
61
  details?: ApprovalRequestDetail[];
62
+ /** Human conversation member who should answer. Defaults to the triggering human. */
63
+ responseUserId?: string;
62
64
  /**
63
65
  * Ignore in-memory session rules for this approval request. Useful when the
64
66
  * action was triggered by someone other than the agent owner.
@@ -80,6 +82,8 @@ export interface RuntimeInputRequest {
80
82
  native?: RuntimeInputNativeMetadata;
81
83
  inputId?: string;
82
84
  timeoutMs?: number;
85
+ /** Human conversation member who should answer. Secret and sudo prompts remain owner-only. */
86
+ responseUserId?: string;
83
87
  }
84
88
  export interface RuntimeInputResult {
85
89
  status: 'submitted' | 'cancelled' | 'timeout';
@@ -91,6 +95,7 @@ export interface RuntimeCardRequest {
91
95
  card: RuntimeCardV1;
92
96
  cardId?: string;
93
97
  expiresAt?: string | number | Date;
98
+ /** Human conversation member who should answer. Defaults to the triggering human. */
94
99
  responseUserId?: string;
95
100
  timeoutMs?: number;
96
101
  runtimeId?: string;
@@ -155,21 +160,21 @@ export interface MessageHandlerContext {
155
160
  /** Runtime turn mode requested by the sender for this inbound turn, if any. */
156
161
  requestedTurnMode: string | null;
157
162
  /**
158
- * Ask the conversation owner to approve a native runtime action. This only
163
+ * Ask the triggering human to approve a native runtime action. This only
159
164
  * renders Canon's inline approval card; runtimes must explicitly wait for
160
165
  * and enforce the returned decision in their own approval hook.
161
166
  */
162
167
  requestApproval: (request: RuntimeApprovalRequest) => Promise<ApprovalResult>;
163
168
  /**
164
- * Ask the agent owner for runtime input such as clarification, sudo, or a
165
- * secret. Sensitive values are returned only to the calling runtime and are
166
- * never persisted in Canon message metadata.
169
+ * Ask the triggering human for runtime input. Secret and sudo prompts remain
170
+ * owner-only. Sensitive values are returned only to the calling runtime and
171
+ * are never persisted in Canon message metadata.
167
172
  */
168
173
  requestRuntimeInput: (request: RuntimeInputRequest) => Promise<RuntimeInputResult>;
169
174
  /**
170
- * Ask the agent owner to review/respond to a generic rich card. The visible
171
- * card document is redacted for presentation; raw response values return
172
- * only to the calling runtime through Canon's control path.
175
+ * Ask the triggering human to review/respond to a generic rich card. The
176
+ * visible card document is redacted for presentation; raw response values
177
+ * return only to the calling runtime through Canon's control path.
173
178
  */
174
179
  requestCard: (request: RuntimeCardRequest) => Promise<RuntimeCardResult>;
175
180
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "5.0.0",
3
+ "version": "5.1.1",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^4.0.0"
31
+ "@canonmsg/core": "^4.2.2"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"