ccp-core 0.4.0 → 0.5.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/runtime.js CHANGED
@@ -4,7 +4,10 @@ import { signingBytes } from "./signing.js";
4
4
  import { keyIdFromPublic, verify } from "./crypto.js";
5
5
  import { decide } from "./decision.js";
6
6
  import { validate as validateSchema } from "./schema.js";
7
- import { AUTHORIZED, CONFIRMATION_REQUIRED } from "./errors.js";
7
+ import { AUTHORIZED, CONFIRMATION_REQUIRED, CcpError } from "./errors.js";
8
+ import { createGrant } from "./grants.js";
9
+ import { verifyManifest } from "./consumer.js";
10
+ import { IdentityError, admit, admittedGrant, verifyIdentityDocument, } from "./identity.js";
8
11
  import { MemoryAuditLog, MemoryGrantStore, MemoryLimitStore, MemoryManifestStore, MemoryPendingStore, MemoryReplayStore, } from "./stores.js";
9
12
  import { utcnow } from "./time.js";
10
13
  export { utcnow };
@@ -20,21 +23,169 @@ export class SchemaRegistry {
20
23
  return id === undefined ? undefined : this._schemas.get(id);
21
24
  }
22
25
  }
26
+ /** The only hosts that may be registered without an explicit profile. */
27
+ export const CORE_HOSTS = new Set([
28
+ "store.read",
29
+ "files.read",
30
+ "files.search",
31
+ "notify",
32
+ "confirm",
33
+ "audit.append",
34
+ ]);
35
+ const HOST_CLASSES = new Set([
36
+ "read",
37
+ "write",
38
+ "disclose",
39
+ "control",
40
+ "compute",
41
+ ]);
42
+ const HOST_RISKS = new Set([
43
+ "read",
44
+ "write",
45
+ "disclose",
46
+ "irreversible",
47
+ ]);
48
+ const HOST_PLACEMENTS = new Set([
49
+ "local",
50
+ "remote",
51
+ "delegated",
52
+ ]);
53
+ const IDEMPOTENCY = new Set([
54
+ "pure",
55
+ "caller_key",
56
+ "atomic",
57
+ "none",
58
+ ]);
59
+ const CLASS_DEFAULTS = {
60
+ read: { risk: "read", minConfirmation: "none", idempotency: "pure" },
61
+ disclose: { risk: "disclose", minConfirmation: "per_action", idempotency: "pure" },
62
+ write: { risk: "write", minConfirmation: "per_action", idempotency: "caller_key" },
63
+ control: { risk: "write", minConfirmation: "per_action", idempotency: "none" },
64
+ compute: { risk: "read", minConfirmation: "per_action", idempotency: "pure" },
65
+ };
66
+ export class HostProfileError extends Error {
67
+ code = "invalid_request";
68
+ constructor(message) {
69
+ super(`invalid_request: ${message}`);
70
+ this.name = "HostProfileError";
71
+ }
72
+ }
73
+ /**
74
+ * Validate and normalize a host profile (§26.4).
75
+ *
76
+ * Extension hosts (anything outside the core six) must declare a class; the
77
+ * rest of the contract follows from it unless explicitly overridden. A
78
+ * `write`, `control`, or `compute` host can never suppress confirmation, and
79
+ * an `irreversible` risk cannot either.
80
+ */
81
+ export function validateHostProfile(name, profile = {}) {
82
+ if (typeof name !== "string" || name === "") {
83
+ throw new HostProfileError("host name is required");
84
+ }
85
+ const declared = profile.class;
86
+ if (declared === undefined && !CORE_HOSTS.has(name)) {
87
+ throw new HostProfileError(`extension host ${name} requires a class`);
88
+ }
89
+ if (declared !== undefined && !HOST_CLASSES.has(declared)) {
90
+ throw new HostProfileError(`unknown host class: ${declared}`);
91
+ }
92
+ const hostClass = (declared ?? "read");
93
+ const defaults = CLASS_DEFAULTS[hostClass];
94
+ const risk = (profile.risk ?? defaults.risk);
95
+ if (!HOST_RISKS.has(risk)) {
96
+ throw new HostProfileError(`unknown host risk: ${risk}`);
97
+ }
98
+ const minConfirmation = (profile.minConfirmation ??
99
+ defaults.minConfirmation);
100
+ if (minConfirmation !== "none" && minConfirmation !== "per_action") {
101
+ throw new HostProfileError(`unknown confirmation floor: ${minConfirmation}`);
102
+ }
103
+ if (minConfirmation === "none") {
104
+ if (hostClass !== "read" && hostClass !== "disclose") {
105
+ throw new HostProfileError(`a ${hostClass} host cannot suppress confirmation`);
106
+ }
107
+ if (risk === "irreversible") {
108
+ throw new HostProfileError("an irreversible host cannot suppress confirmation");
109
+ }
110
+ }
111
+ const idempotency = (profile.idempotency ?? defaults.idempotency);
112
+ if (!IDEMPOTENCY.has(idempotency)) {
113
+ throw new HostProfileError(`unknown idempotency: ${idempotency}`);
114
+ }
115
+ const network = profile.network === true;
116
+ if (profile.network !== undefined && typeof profile.network !== "boolean") {
117
+ throw new HostProfileError("network must be a boolean");
118
+ }
119
+ const placement = profile.placement ?? ["local"];
120
+ if (!Array.isArray(placement) || placement.length === 0) {
121
+ throw new HostProfileError("placement must be a non-empty array");
122
+ }
123
+ const seenPlacement = new Set();
124
+ for (const entry of placement) {
125
+ if (typeof entry !== "string" || !HOST_PLACEMENTS.has(entry)) {
126
+ throw new HostProfileError(`unknown placement: ${entry}`);
127
+ }
128
+ if (seenPlacement.has(entry)) {
129
+ throw new HostProfileError(`duplicate placement: ${entry}`);
130
+ }
131
+ seenPlacement.add(entry);
132
+ }
133
+ const fields = profile.fields ?? [];
134
+ if (!Array.isArray(fields)) {
135
+ throw new HostProfileError("fields must be an array");
136
+ }
137
+ const seenFields = new Set();
138
+ for (const field of fields) {
139
+ if (typeof field !== "string" || field === "") {
140
+ throw new HostProfileError("fields must be non-empty strings");
141
+ }
142
+ if (seenFields.has(field)) {
143
+ throw new HostProfileError(`duplicate field: ${field}`);
144
+ }
145
+ seenFields.add(field);
146
+ }
147
+ if (network && fields.length === 0 && hostClass === "disclose") {
148
+ throw new HostProfileError("a network disclose host must declare the fields it sends");
149
+ }
150
+ return {
151
+ class: hostClass,
152
+ risk,
153
+ minConfirmation,
154
+ idempotency,
155
+ network,
156
+ placement: placement,
157
+ fields: [...fields],
158
+ };
159
+ }
23
160
  export class HostAccess {
24
161
  registry;
162
+ policy;
25
163
  _granted;
26
- constructor(registry, granted) {
164
+ constructor(registry, granted, policy = {
165
+ network: false,
166
+ fields: new Set(),
167
+ }) {
27
168
  this.registry = registry;
169
+ this.policy = policy;
28
170
  this._granted = new Set(granted);
29
171
  }
30
172
  call(name, ...args) {
31
173
  if (!this._granted.has(name)) {
32
174
  throw new Error(`host function not granted: ${name}`);
33
175
  }
34
- const fn = this.registry.host(name);
35
- if (!fn)
176
+ const entry = this.registry.hostMeta(name);
177
+ if (entry === undefined) {
36
178
  throw new Error(`host function not installed: ${name}`);
37
- return fn(...args);
179
+ }
180
+ if (entry.profile.network && !this.policy.network) {
181
+ throw new Error(`host requires network egress not granted: ${name}`);
182
+ }
183
+ for (const field of entry.profile.fields) {
184
+ if (!this.policy.fields.has(field)) {
185
+ throw new Error(`host may send an undeclared field: ${name}/${field}`);
186
+ }
187
+ }
188
+ return entry.fn(...args);
38
189
  }
39
190
  has(name) {
40
191
  return this._granted.has(name) && this.registry.hasHost(name);
@@ -43,13 +194,20 @@ export class HostAccess {
43
194
  export class HostRegistry {
44
195
  _hosts = new Map();
45
196
  _intents = new Map();
46
- registerHost(name, fn) {
47
- this._hosts.set(name, fn);
197
+ registerHost(name, fn, profile = {}) {
198
+ this._hosts.set(name, {
199
+ fn,
200
+ profile: validateHostProfile(name, profile),
201
+ });
48
202
  }
49
203
  registerIntent(name, fn) {
50
204
  this._intents.set(name, fn);
51
205
  }
52
206
  host(name) {
207
+ return this._hosts.get(name)?.fn;
208
+ }
209
+ /** Metadata used to enforce placement, egress, and confirmation (§12). */
210
+ hostMeta(name) {
53
211
  return this._hosts.get(name);
54
212
  }
55
213
  hasHost(name) {
@@ -61,10 +219,16 @@ export class HostRegistry {
61
219
  hasIntent(name) {
62
220
  return this._intents.has(name);
63
221
  }
64
- access(granted) {
65
- return new HostAccess(this, granted);
222
+ access(granted, policy) {
223
+ return new HostAccess(this, granted, policy);
66
224
  }
67
225
  }
226
+ /**
227
+ * Reserved suspension reason for a requester key change (§7.4). A grant
228
+ * suspended this way cannot be resumed; the consumer re-introduces the
229
+ * contact and creates a new grant.
230
+ */
231
+ export const IDENTITY_CHANGED_REASON = "identity_changed";
68
232
  export class Runtime {
69
233
  grants;
70
234
  manifests;
@@ -83,7 +247,7 @@ export class Runtime {
83
247
  this.hosts = options.hosts ?? new HostRegistry();
84
248
  this.audit = options.audit ?? new MemoryAuditLog();
85
249
  this.clock = options.clock ?? utcnow;
86
- this.supportedCcp = options.supportedCcp ?? "0.4-draft";
250
+ this.supportedCcp = options.supportedCcp ?? ["0.5-draft", "0.4-draft"];
87
251
  this.replay = options.replay ?? new MemoryReplayStore();
88
252
  this.limits = options.limits ?? new MemoryLimitStore();
89
253
  this.pendingStore = options.pending ?? new MemoryPendingStore();
@@ -123,7 +287,7 @@ export class Runtime {
123
287
  return this.reject("unauthenticated", signal.request_id, undefined, String(error));
124
288
  }
125
289
  const signer = keyIdFromPublic(publicKey);
126
- const grant = await this.grants.find(signer, signal.grant_digest);
290
+ const grant = await this.resolveGrant(signer, signal.grant_digest, signal.intent);
127
291
  let intentSpec = null;
128
292
  let paramsSchema = null;
129
293
  if (grant) {
@@ -148,7 +312,7 @@ export class Runtime {
148
312
  supportedCcp: this.supportedCcp,
149
313
  });
150
314
  if (outcome !== AUTHORIZED && outcome !== CONFIRMATION_REQUIRED) {
151
- return this.reject(outcome, signal.request_id, grant);
315
+ return this.reject(outcome, signal.request_id, grant, this.stateDetail(outcome, grant));
152
316
  }
153
317
  if (!(await this.replay.markIfNew(signal.request_id))) {
154
318
  return this.reject("replay_detected", signal.request_id, grant);
@@ -162,7 +326,13 @@ export class Runtime {
162
326
  await this.limits.release(grant.id, grant.limits);
163
327
  return this.reject("capability_unavailable", signal.request_id, grant, `unavailable: ${unavailable.length > 0 ? unavailable.join(",") : signal.intent}`);
164
328
  }
165
- if (outcome === CONFIRMATION_REQUIRED) {
329
+ const deployment = this.deploymentError(grant, intentSpec, required);
330
+ if (deployment !== undefined) {
331
+ await this.limits.release(grant.id, grant.limits);
332
+ return this.reject("capability_unavailable", signal.request_id, grant, deployment);
333
+ }
334
+ const requiresConfirmation = outcome === CONFIRMATION_REQUIRED || this.hostFloor(intentSpec);
335
+ if (requiresConfirmation) {
166
336
  const pendingId = randomId();
167
337
  await this.pendingStore.put({
168
338
  id: pendingId,
@@ -204,16 +374,206 @@ export class Runtime {
204
374
  state: "cancelled",
205
375
  };
206
376
  }
207
- const stateError = this.grantStateError(pending.grant, this.clock());
377
+ // The grant may have been revoked or paused while the prompt was open
378
+ // (§9.4.3); re-read it rather than trusting the snapshot.
379
+ const current = (await this.grants.get(pending.grant.id)) ?? pending.grant;
380
+ const stateError = this.grantStateError(current, this.clock());
208
381
  if (stateError) {
209
- await this.limits.release(pending.grant.id, pending.grant.limits);
210
- return this.reject(stateError, pending.signal.request_id, pending.grant);
382
+ await this.limits.release(current.id, current.limits);
383
+ return this.reject(stateError, pending.signal.request_id, current, this.stateDetail(stateError, current));
211
384
  }
212
- return this.execute(pending.signal, pending.grant, pending.intentSpec);
385
+ return this.execute(pending.signal, current, pending.intentSpec);
213
386
  }
214
387
  pending(pendingId) {
215
388
  return Promise.resolve(this.pendingStore.get(pendingId));
216
389
  }
390
+ /**
391
+ * Pause one grant (§9.4.1). Configuration, limits, and history are kept.
392
+ */
393
+ async pauseGrant(id, reason) {
394
+ return this.transition(id, "paused", reason);
395
+ }
396
+ /**
397
+ * Suspend one grant (§9.4.2). Used for security events such as a requester
398
+ * key change; resumption requires an explicit `resumeGrant`.
399
+ */
400
+ async suspendGrant(id, reason) {
401
+ return this.transition(id, "suspended", reason);
402
+ }
403
+ /**
404
+ * Resume a paused or suspended grant (§9.4.3).
405
+ *
406
+ * Re-checks revocation, expiry, and the approved manifest before restoring
407
+ * authority. A revoked or expired grant cannot be resumed.
408
+ */
409
+ async resumeGrant(id) {
410
+ const grant = await this.grants.get(id);
411
+ if (grant === undefined) {
412
+ throw new CcpError("no_matching_grant", `unknown grant: ${id}`);
413
+ }
414
+ if (grant.state === "revoked") {
415
+ throw new CcpError("grant_revoked", "a revoked grant cannot be resumed");
416
+ }
417
+ if (grant.state === "suspended" &&
418
+ grant.state_reason === IDENTITY_CHANGED_REASON) {
419
+ throw new CcpError("grant_suspended", "identity changed; re-introduce the contact and create a new grant");
420
+ }
421
+ if (grant.state === "expired" || this.clock() >= grant.expires_at) {
422
+ throw new CcpError("grant_expired", "an expired grant cannot be resumed");
423
+ }
424
+ if (this.manifests.get(grant.manifest_digest) === undefined) {
425
+ throw new CcpError("capability_unavailable", "the approved manifest is no longer stored");
426
+ }
427
+ if (grant.state === "active")
428
+ return grant;
429
+ return this.transition(id, "active", undefined);
430
+ }
431
+ /**
432
+ * Revoke one grant by id (§9.4.4). Permanent: the grant is not resumable and
433
+ * a new approval is required.
434
+ *
435
+ * Policy-created grants reuse their deterministic id, so revoking here also
436
+ * stops an admission rule from re-creating the same access (§25.8.3).
437
+ */
438
+ async revokeGrant(id, reason) {
439
+ return this.transition(id, "revoked", reason);
440
+ }
441
+ /**
442
+ * Suspend every active grant bound to a contact (§7.4, §9.4.2).
443
+ * Returns the number of grants affected.
444
+ */
445
+ async suspendContact(contactId, reason) {
446
+ const affected = (await this.grants.all()).filter((grant) => grant.contact_id === contactId && grant.state !== "revoked");
447
+ for (const grant of affected) {
448
+ await this.setGrantState(grant, "suspended", reason ?? "contact suspended");
449
+ }
450
+ return affected.length;
451
+ }
452
+ /**
453
+ * Verify an identity document for a known contact (§7.4, §25.8.4).
454
+ *
455
+ * On `identity_changed` — a valid signature from a different key — the
456
+ * contact's grants are suspended before the error is rethrown. A bad
457
+ * signature never suspends anything, so an attacker cannot force a
458
+ * suspension by sending garbage.
459
+ */
460
+ async verifyContactIdentity(document, options) {
461
+ try {
462
+ return verifyIdentityDocument(document, options);
463
+ }
464
+ catch (error) {
465
+ if (error instanceof IdentityError && error.code === "identity_changed") {
466
+ const suspended = await this.suspendContact(options.contactId, IDENTITY_CHANGED_REASON);
467
+ await this.audit.append({
468
+ event: "identity_changed",
469
+ contact_id: options.contactId,
470
+ detail: `${suspended} grant(s) suspended`,
471
+ });
472
+ }
473
+ throw error;
474
+ }
475
+ }
476
+ /**
477
+ * Admit a request under a reviewed policy (§25.8.3).
478
+ *
479
+ * `allow` creates or reuses the grant with the policy's deterministic id,
480
+ * records the admitting rule in the audit log, and leaves the request to
481
+ * proceed through `submit` normally. Revoked ids are consulted from the
482
+ * grant store, so a revocation is never undone here.
483
+ */
484
+ async admitRequest(options) {
485
+ const { policy, request } = options;
486
+ const manifest = typeof options.manifest === "string"
487
+ ? verifyManifest(options.manifest)
488
+ : options.manifest;
489
+ const revoked = new Set((await this.grants.all())
490
+ .filter((grant) => grant.state === "revoked")
491
+ .map((grant) => grant.id));
492
+ const result = admit(policy, request, { revoked });
493
+ if (result.decision !== "allow")
494
+ return { result };
495
+ const granted = admittedGrant(policy, request, result);
496
+ if (granted === undefined)
497
+ return { result };
498
+ // A policy may never admit an intent whose hosts require a human.
499
+ const intentSpec = manifest.manifest.intents?.[granted.intent];
500
+ for (const capability of intentSpec?.required_capabilities ?? []) {
501
+ const meta = this.hosts.hostMeta(capability);
502
+ if (meta === undefined) {
503
+ return { result: { decision: "deny", reason: "capability_unavailable" } };
504
+ }
505
+ if (meta.profile.minConfirmation !== "none") {
506
+ return { result: { decision: "deny", reason: "risk_not_admittable" } };
507
+ }
508
+ }
509
+ const existing = await this.grants.get(granted.id);
510
+ if (existing !== undefined)
511
+ return { result, grant: existing };
512
+ const grant = createGrant({
513
+ id: granted.id,
514
+ contactId: request.contactId,
515
+ requesterKey: request.requesterKey,
516
+ manifest,
517
+ intents: [granted.intent],
518
+ limits: granted.limits,
519
+ confirmation: options.confirmation ?? { [granted.intent]: "none" },
520
+ createdAt: this.clock(),
521
+ expiresAt: granted.expiresAt,
522
+ admittedBy: result.ruleId,
523
+ });
524
+ await this.grants.add(grant);
525
+ this.manifests.add(manifest);
526
+ await this.audit.append({
527
+ event: "admitted",
528
+ grant_id: grant.id,
529
+ intent: granted.intent,
530
+ rule_id: result.ruleId,
531
+ });
532
+ return { result, grant };
533
+ }
534
+ /**
535
+ * Resolve the grant for this request.
536
+ *
537
+ * A requester may hold several grants over one manifest digest (for
538
+ * example, a manual grant and a policy-created one). The store lookup is
539
+ * the fast path; when it lands on a grant that does not enable the intent,
540
+ * prefer a grant that does. Revocation remains per grant (§25.8.3).
541
+ */
542
+ async resolveGrant(requesterKey, manifestDigest, intent) {
543
+ const found = await this.grants.find(requesterKey, manifestDigest);
544
+ if (found !== undefined && found.intents.includes(intent))
545
+ return found;
546
+ const candidates = (await this.grants.all()).filter((grant) => grant.requester_key === requesterKey &&
547
+ grant.manifest_digest === manifestDigest &&
548
+ grant.intents.includes(intent));
549
+ if (candidates.length === 0)
550
+ return found;
551
+ return candidates.find((grant) => grant.state === "active") ?? candidates[0];
552
+ }
553
+ async transition(id, state, reason) {
554
+ const grant = await this.grants.get(id);
555
+ if (grant === undefined) {
556
+ throw new CcpError("no_matching_grant", `unknown grant: ${id}`);
557
+ }
558
+ if (grant.state === "revoked" && state !== "revoked") {
559
+ throw new CcpError("grant_revoked", "a revoked grant is final");
560
+ }
561
+ return this.setGrantState(grant, state, reason);
562
+ }
563
+ async setGrantState(grant, state, reason) {
564
+ const updated = {
565
+ ...grant,
566
+ state,
567
+ state_reason: state === "active" ? null : reason ?? grant.state_reason ?? null,
568
+ };
569
+ await this.grants.add(updated);
570
+ await this.audit.append({
571
+ event: state === "active" ? "resumed" : state,
572
+ grant_id: grant.id,
573
+ detail: reason,
574
+ });
575
+ return updated;
576
+ }
217
577
  async execute(signal, grant, intentSpec) {
218
578
  await this.audit.append({
219
579
  event: "executing",
@@ -234,7 +594,7 @@ export class Runtime {
234
594
  }
235
595
  let result;
236
596
  try {
237
- result = await executor(signal.params, this.hosts.access(grant.capabilities));
597
+ result = await executor(signal.params, this.hosts.access(grant.capabilities, this.egressPolicy(grant)));
238
598
  }
239
599
  catch (error) {
240
600
  await this.limits.release(grant.id, grant.limits);
@@ -284,6 +644,51 @@ export class Runtime {
284
644
  state: "completed",
285
645
  };
286
646
  }
647
+ /**
648
+ * Enforce the grant's placement and egress ceilings before execution (§12).
649
+ * A host that needs network access cannot run under a grant that denies it.
650
+ */
651
+ deploymentError(grant, intentSpec, required) {
652
+ const placement = typeof intentSpec.placement === "string" ? intentSpec.placement : "local";
653
+ if (!(grant.placement ?? []).includes(placement)) {
654
+ return `placement ${placement} is not granted`;
655
+ }
656
+ const policy = this.egressPolicy(grant);
657
+ for (const capability of required) {
658
+ const meta = this.hosts.hostMeta(capability);
659
+ if (meta === undefined)
660
+ return `host ${capability} is not installed`;
661
+ if (!meta.profile.placement.includes(placement)) {
662
+ return `host ${capability} cannot run with ${placement} placement`;
663
+ }
664
+ if (meta.profile.network && !policy.network) {
665
+ return `host ${capability} requires network egress not granted`;
666
+ }
667
+ for (const field of meta.profile.fields) {
668
+ if (!policy.fields.has(field)) {
669
+ return `host ${capability} may send undeclared field ${field}`;
670
+ }
671
+ }
672
+ }
673
+ return undefined;
674
+ }
675
+ /** True when a required host declares a per-action confirmation floor. */
676
+ hostFloor(intentSpec) {
677
+ for (const capability of intentSpec.required_capabilities ?? []) {
678
+ const meta = this.hosts.hostMeta(capability);
679
+ if (meta?.profile.minConfirmation === "per_action")
680
+ return true;
681
+ }
682
+ return false;
683
+ }
684
+ egressPolicy(grant) {
685
+ const egress = grant.egress ?? {};
686
+ const fields = Array.isArray(egress.fields) ? egress.fields : [];
687
+ return {
688
+ network: egress.network === true,
689
+ fields: new Set(fields.filter((field) => typeof field === "string")),
690
+ };
691
+ }
287
692
  grantStateError(grant, now) {
288
693
  if (grant.state === "revoked")
289
694
  return "grant_revoked";
@@ -296,6 +701,20 @@ export class Runtime {
296
701
  }
297
702
  return undefined;
298
703
  }
704
+ /**
705
+ * Surface a recorded pause/suspend/revoke reason (§9.4.2). The reason is
706
+ * local and must not contain secrets; hosts choose what to record.
707
+ */
708
+ stateDetail(outcome, grant) {
709
+ if (grant === undefined)
710
+ return undefined;
711
+ if (outcome === "grant_revoked" ||
712
+ outcome === "grant_paused" ||
713
+ outcome === "grant_suspended") {
714
+ return grant.state_reason ?? undefined;
715
+ }
716
+ return undefined;
717
+ }
299
718
  async reject(code, requestId, grant, detail) {
300
719
  await this.audit.append({
301
720
  event: "rejected",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccp-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",