@scopebond/mcp 0.2.0 → 0.3.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/dist/proxy.d.ts CHANGED
@@ -18,6 +18,19 @@ export declare function mapMcpToolCall(server: string, params: Record<string, un
18
18
  export interface McpUpstream {
19
19
  call(message: JsonRpcMessage): Promise<JsonRpcMessage>;
20
20
  }
21
+ /** The minimal receipt shape a windowed clause needs to count a prior authorized
22
+ * call. The proxy keeps these for the session so rate_limit/sequence clauses work. */
23
+ export interface CountableCall {
24
+ intent: {
25
+ action_type: string;
26
+ params?: Record<string, unknown>;
27
+ asset?: string;
28
+ amount?: number;
29
+ };
30
+ executed: boolean;
31
+ timestamp: string;
32
+ intent_hash: string;
33
+ }
21
34
  export interface McpProxyConfig {
22
35
  policy: unknown;
23
36
  /** The validated caller identity recorded on every receipt. */
@@ -33,6 +46,11 @@ export interface McpProxyConfig {
33
46
  now?: () => string;
34
47
  /** Sink for each emitted receipt (e.g. a local log / exporter). */
35
48
  onReceipt?: (receipt: SignedReceipt) => void | Promise<void>;
49
+ /** Prior authorized calls to seed the window with (e.g. loaded from a durable log
50
+ * at startup). The proxy also accumulates in-session calls on top of these so
51
+ * rate_limit, spend_limit and sequence clauses see the real history rather than
52
+ * an empty one. */
53
+ history?: CountableCall[];
36
54
  }
37
55
  export interface McpProxy {
38
56
  handle(message: JsonRpcMessage): Promise<JsonRpcMessage>;
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,oBAAoB,CAAC;AAIlE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID;6BAC6B;AAC7B,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAKpJ;AAED;mCACmC;AACnC,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,WAAW,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mEAAmE;IACnE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC1D;AAED;uDACuD;AACvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CAgC/D"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,oBAAoB,CAAC;AAIlE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID;6BAC6B;AAC7B,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAKpJ;AAED;mCACmC;AACnC,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACxD;AAED;uFACuF;AACvF,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnG,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,WAAW,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mEAAmE;IACnE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D;;;wBAGoB;IACpB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC1D;AAED;uDACuD;AACvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CAiC/D"}
package/dist/proxy.js CHANGED
@@ -22,18 +22,17 @@ export function mapMcpToolCall(server, params) {
22
22
  * else is passed through to the upstream unchanged. */
23
23
  export function createMcpProxy(config) {
24
24
  const attester = attesterFromPrivateKeyPem(config.attesterKeyPem);
25
+ // Session history of authorized calls. A forwarded call actually runs, so it
26
+ // counts as executed for windowed clauses (rate_limit, sequence, spend_limit).
27
+ const history = [...(config.history ?? [])];
25
28
  return {
26
29
  async handle(message) {
27
30
  if (message?.method !== "tools/call")
28
31
  return config.upstream.call(message);
29
32
  const intent = mapMcpToolCall(config.server, message.params);
30
- const claimed = {
31
- intent,
32
- executed: true,
33
- timestamp: config.now?.() ?? new Date().toISOString(),
34
- intent_hash: digest(intent).slice(7),
35
- };
36
- const verdict = violates(config.policy, [], claimed, {});
33
+ const timestamp = config.now?.() ?? new Date().toISOString();
34
+ const claimed = { intent, executed: true, timestamp, intent_hash: digest(intent).slice(7) };
35
+ const verdict = violates(config.policy, history, claimed, { at: timestamp });
37
36
  const decision = verdict.violated ? "deny" : "allow";
38
37
  const receipt = await buildPepReceipt({ intent, policy: config.policy, principal: config.principal, realtimeResult: decision, now: config.now }, attester);
39
38
  await config.onReceipt?.(receipt);
@@ -44,6 +43,8 @@ export function createMcpProxy(config) {
44
43
  error: { code: -32000, message: `Scopebond policy denied ${intent.params.tool}: ${verdict.explanation || "out of policy"}` },
45
44
  };
46
45
  }
46
+ // Authorized and about to be forwarded: record it so it counts toward the window.
47
+ history.push(claimed);
47
48
  return config.upstream.call(message);
48
49
  },
49
50
  };
package/dist/proxy.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,sEAAsE;AACtE,8EAA8E;AAC9E,yEAAyE;AACzE,+EAA+E;AAC/E,iFAAiF;AACjF,gFAAgF;AAEhF,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAWzC,MAAM,MAAM,GAAG,CAAC,KAAc,EAAU,EAAE,CAAC,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAc,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAE5H;6BAC6B;AAC7B,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,MAA2C;IACxF,OAAO;QACL,WAAW,EAAE,eAAe;QAC5B,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,EAAE;KACnG,CAAC;AACJ,CAAC;AA0BD;uDACuD;AACvD,MAAM,UAAU,cAAc,CAAC,MAAsB;IACnD,MAAM,QAAQ,GAAa,yBAAyB,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAC5E,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,OAAuB;YAClC,IAAI,OAAO,EAAE,MAAM,KAAK,YAAY;gBAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAE3E,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAC7D,MAAM,OAAO,GAAG;gBACd,MAAM;gBACN,QAAQ,EAAE,IAAI;gBACd,SAAS,EAAE,MAAM,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACrD,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;aACrC,CAAC;YACF,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAe,EAAE,EAAE,EAAE,OAAgB,EAAE,EAAE,CAAC,CAAC;YAC3E,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;YAErD,MAAM,OAAO,GAAG,MAAM,eAAe,CACnC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAe,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,EAClH,QAAQ,CACT,CAAC;YACF,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;YAElC,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;gBACxB,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,IAAI;oBACtB,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,2BAA2B,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC,WAAW,IAAI,eAAe,EAAE,EAAE;iBAC7H,CAAC;YACJ,CAAC;YACD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvC,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["// The MCP proxy core: sit in front of an upstream MCP server, check each\n// `tools/call` against policy before forwarding it, and emit a signed\n// PEP-authorized receipt. A denied call is never forwarded. Deterministic and\n// transport-agnostic — the upstream is injected, so the decision path is\n// conformance-tested directly; the `scopebond-mcp` CLI provides the real stdio\n// transport. The proxy decides the caller's request (no agent signature), so its\n// receipts are pep_authorized (§15) — the identity is the configured principal.\n\nimport { violates } from \"@scopebond/verify\";\nimport { buildPepReceipt, attesterFromPrivateKeyPem } from \"@scopebond/gateway\";\nimport type { SignedReceipt, Attester } from \"@scopebond/gateway\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\nimport { createHash } from \"node:crypto\";\n\nexport interface JsonRpcMessage {\n jsonrpc?: string;\n id?: string | number | null;\n method?: string;\n params?: Record<string, unknown>;\n result?: unknown;\n error?: unknown;\n}\n\nconst digest = (value: unknown): string => \"sha256:\" + createHash(\"sha256\").update(canonical(value as never)).digest(\"hex\");\n\n/** Map an MCP `tools/call` to a normalized `mcp.tool.call` action. Arguments are\n * digested, never stored. */\nexport function mapMcpToolCall(server: string, params: Record<string, unknown> | undefined): { action_type: string; params: Record<string, unknown> } {\n return {\n action_type: \"mcp.tool.call\",\n params: { server, tool: String(params?.name ?? \"\"), args_digest: digest(params?.arguments ?? {}) },\n };\n}\n\n/** A pre-authenticated forwarder to the upstream MCP server. Tests inject it; the\n * CLI implements it over stdio. */\nexport interface McpUpstream {\n call(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\nexport interface McpProxyConfig {\n policy: unknown;\n /** The validated caller identity recorded on every receipt. */\n principal: { subject: string; issuer: string };\n /** The upstream server id, used as the action's `server` parameter. */\n server: string;\n /** PEM of the key that signs the PEP receipts (the enrolled proxy key). */\n attesterKeyPem: string;\n upstream: McpUpstream;\n now?: () => string;\n /** Sink for each emitted receipt (e.g. a local log / exporter). */\n onReceipt?: (receipt: SignedReceipt) => void | Promise<void>;\n}\n\nexport interface McpProxy {\n handle(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\n/** Build a proxy handler. Every `tools/call` is decided against policy; anything\n * else is passed through to the upstream unchanged. */\nexport function createMcpProxy(config: McpProxyConfig): McpProxy {\n const attester: Attester = attesterFromPrivateKeyPem(config.attesterKeyPem);\n return {\n async handle(message: JsonRpcMessage): Promise<JsonRpcMessage> {\n if (message?.method !== \"tools/call\") return config.upstream.call(message);\n\n const intent = mapMcpToolCall(config.server, message.params);\n const claimed = {\n intent,\n executed: true,\n timestamp: config.now?.() ?? new Date().toISOString(),\n intent_hash: digest(intent).slice(7),\n };\n const verdict = violates(config.policy as never, [], claimed as never, {});\n const decision = verdict.violated ? \"deny\" : \"allow\";\n\n const receipt = await buildPepReceipt(\n { intent, policy: config.policy as never, principal: config.principal, realtimeResult: decision, now: config.now },\n attester,\n );\n await config.onReceipt?.(receipt);\n\n if (decision === \"deny\") {\n return {\n jsonrpc: \"2.0\",\n id: message.id ?? null,\n error: { code: -32000, message: `Scopebond policy denied ${intent.params.tool}: ${verdict.explanation || \"out of policy\"}` },\n };\n }\n return config.upstream.call(message);\n },\n };\n}\n"]}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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","sourcesContent":["// The MCP proxy core: sit in front of an upstream MCP server, check each\n// `tools/call` against policy before forwarding it, and emit a signed\n// PEP-authorized receipt. A denied call is never forwarded. Deterministic and\n// transport-agnostic — the upstream is injected, so the decision path is\n// conformance-tested directly; the `scopebond-mcp` CLI provides the real stdio\n// transport. The proxy decides the caller's request (no agent signature), so its\n// receipts are pep_authorized (§15) — the identity is the configured principal.\n\nimport { violates } from \"@scopebond/verify\";\nimport { buildPepReceipt, attesterFromPrivateKeyPem } from \"@scopebond/gateway\";\nimport type { SignedReceipt, Attester } from \"@scopebond/gateway\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\nimport { createHash } from \"node:crypto\";\n\nexport interface JsonRpcMessage {\n jsonrpc?: string;\n id?: string | number | null;\n method?: string;\n params?: Record<string, unknown>;\n result?: unknown;\n error?: unknown;\n}\n\nconst digest = (value: unknown): string => \"sha256:\" + createHash(\"sha256\").update(canonical(value as never)).digest(\"hex\");\n\n/** Map an MCP `tools/call` to a normalized `mcp.tool.call` action. Arguments are\n * digested, never stored. */\nexport function mapMcpToolCall(server: string, params: Record<string, unknown> | undefined): { action_type: string; params: Record<string, unknown> } {\n return {\n action_type: \"mcp.tool.call\",\n params: { server, tool: String(params?.name ?? \"\"), args_digest: digest(params?.arguments ?? {}) },\n };\n}\n\n/** A pre-authenticated forwarder to the upstream MCP server. Tests inject it; the\n * CLI implements it over stdio. */\nexport interface McpUpstream {\n call(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\n/** The minimal receipt shape a windowed clause needs to count a prior authorized\n * call. The proxy keeps these for the session so rate_limit/sequence clauses work. */\nexport interface CountableCall {\n intent: { action_type: string; params?: Record<string, unknown>; asset?: string; amount?: number };\n executed: boolean;\n timestamp: string;\n intent_hash: string;\n}\n\nexport interface McpProxyConfig {\n policy: unknown;\n /** The validated caller identity recorded on every receipt. */\n principal: { subject: string; issuer: string };\n /** The upstream server id, used as the action's `server` parameter. */\n server: string;\n /** PEM of the key that signs the PEP receipts (the enrolled proxy key). */\n attesterKeyPem: string;\n upstream: McpUpstream;\n now?: () => string;\n /** Sink for each emitted receipt (e.g. a local log / exporter). */\n onReceipt?: (receipt: SignedReceipt) => void | Promise<void>;\n /** Prior authorized calls to seed the window with (e.g. loaded from a durable log\n * at startup). The proxy also accumulates in-session calls on top of these so\n * rate_limit, spend_limit and sequence clauses see the real history rather than\n * an empty one. */\n history?: CountableCall[];\n}\n\nexport interface McpProxy {\n handle(message: JsonRpcMessage): Promise<JsonRpcMessage>;\n}\n\n/** Build a proxy handler. Every `tools/call` is decided against policy; anything\n * else is passed through to the upstream unchanged. */\nexport function createMcpProxy(config: McpProxyConfig): McpProxy {\n const attester: Attester = attesterFromPrivateKeyPem(config.attesterKeyPem);\n // Session history of authorized calls. A forwarded call actually runs, so it\n // counts as executed for windowed clauses (rate_limit, sequence, spend_limit).\n const history: CountableCall[] = [...(config.history ?? [])];\n return {\n async handle(message: JsonRpcMessage): Promise<JsonRpcMessage> {\n if (message?.method !== \"tools/call\") return config.upstream.call(message);\n\n const intent = mapMcpToolCall(config.server, message.params);\n const timestamp = config.now?.() ?? new Date().toISOString();\n const claimed = { intent, executed: true, timestamp, intent_hash: digest(intent).slice(7) };\n const verdict = violates(config.policy as never, history as never, claimed as never, { at: timestamp });\n const decision = verdict.violated ? \"deny\" : \"allow\";\n\n const receipt = await buildPepReceipt(\n { intent, policy: config.policy as never, principal: config.principal, realtimeResult: decision, now: config.now },\n attester,\n );\n await config.onReceipt?.(receipt);\n\n if (decision === \"deny\") {\n return {\n jsonrpc: \"2.0\",\n id: message.id ?? null,\n error: { code: -32000, message: `Scopebond policy denied ${intent.params.tool}: ${verdict.explanation || \"out of policy\"}` },\n };\n }\n // Authorized and about to be forwarded: record it so it counts toward the window.\n history.push(claimed);\n return config.upstream.call(message);\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scopebond/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Scopebond MCP proxy — one policy for every Model Context Protocol tool call, in front of the servers an agent uses. In-path enforcement with a signed PEP-authorized receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Avouro LLC",
@@ -40,9 +40,9 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
- "@scopebond/policy-schema": "^0.3.0",
44
- "@scopebond/verify": "^0.2.0",
45
- "@scopebond/gateway": "^0.5.0"
43
+ "@scopebond/policy-schema": "^0.4.0",
44
+ "@scopebond/verify": "^0.3.0",
45
+ "@scopebond/gateway": "^0.6.1"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"