@civitai/blocks-react 0.73.1 → 0.74.0

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
@@ -1753,8 +1753,10 @@ divergences:
1753
1753
  - the byte gate refusing a shrinking overwrite that the host admits
1754
1754
  ([#345](https://github.com/civitai/civitai-app-starters/issues/345));
1755
1755
  - 🔴 the byte gate counting **wire** bytes where the host counts **stored**
1756
- bytes — `octet_length(value::jsonb::text)`, larger for every container, up to
1757
- ~1.5x ([#347](https://github.com/civitai/civitai-app-starters/issues/347));
1756
+ bytes — `octet_length(value::jsonb::text)`, larger for every container: ~1.5x
1757
+ for a long array of small integers, and measured up to 44.4x for a
1758
+ numeric-heavy one (an array of `1e308`)
1759
+ ([#347](https://github.com/civitai/civitai-app-starters/issues/347));
1758
1760
  - nothing models the **app-wide** umbrella, so `app quota exceeded` and `app row
1759
1761
  limit exceeded` cannot be produced here at all
1760
1762
  ([#368](https://github.com/civitai/civitai-app-starters/issues/368));
@@ -1901,11 +1903,6 @@ Only writes that **land** spend the `append`/`update` budget: a write that fails
1901
1903
  without landing gets its token back. The few failures the platform marks as its
1902
1904
  own fault are refunded without a rejection charge too.
1903
1905
 
1904
- > The `append`/`update` budget above goes live with the next civitai.com
1905
- > release. Until then the live limit is a fixed window of **20 posts + edits a
1906
- > day** per (viewer, app). The vote, withdraw and report limits, and the retry
1907
- > metadata below, are already live.
1908
-
1909
1906
  A write refused for budget arrives with retry metadata, and the hook rejects with
1910
1907
  **`SharedStorageRateLimitError`**. It extends the plain `Error` every other
1911
1908
  refusal is thrown as, so existing `catch` code keeps working, and `.message` is
@@ -3016,11 +3013,13 @@ from `@civitai/app-sdk/blocks` is the same thing without the hook.
3016
3013
  ## The `/ui` subexport
3017
3014
 
3018
3015
  Opinionated components, imported separately so a transport-only block stays lean.
3019
- Two surfaces live here:
3016
+ Three surfaces live here:
3020
3017
 
3021
3018
  1. The **W6 component pack** — a small, Civitai-looking, self-styled component
3022
3019
  set you drop straight into a block.
3023
- 2. The headless, manifest-driven **`SettingsForm`** (host-themed native controls).
3020
+ 2. **`WorkflowPicker`** — lets the viewer pick a posted workflow, theirs or
3021
+ anyone's, from the Civitai catalog.
3022
+ 3. The headless, manifest-driven **`SettingsForm`** (host-themed native controls).
3024
3023
 
3025
3024
  ### W6 component pack
3026
3025
 
@@ -3112,6 +3111,90 @@ to inject the CSS yourself (SSR, or a non-React shell)? Call
3112
3111
  > inside the sandboxed block; a full focus-trap is a v1 follow-up (kept
3113
3112
  > dependency-free here on purpose).
3114
3113
 
3114
+ ### `WorkflowPicker`
3115
+
3116
+ `<civitai-resource-picker>` from `@civitai/components`, set up for workflows: it
3117
+ lists `Workflows` and the retired `ComfyWorkflows` type (so older posts can be
3118
+ picked), lets the viewer switch between **their own** published workflows and
3119
+ **anyone's**, and picks one **version** (the newest by default). It calls
3120
+ `GET /api/v1/blocks/models` at `useHostOrigin()` with `useBlockToken().raw` —
3121
+ no host bridge, no extra scope.
3122
+
3123
+ ```tsx
3124
+ import { useState } from 'react';
3125
+ import { WorkflowPicker, type PickedResource } from '@civitai/blocks-react/ui';
3126
+
3127
+ export function ChooseWorkflow() {
3128
+ const [open, setOpen] = useState(false);
3129
+ const [picked, setPicked] = useState<PickedResource | null>(null);
3130
+ return (
3131
+ <>
3132
+ <button type="button" onClick={() => setOpen(true)}>Use a workflow</button>
3133
+ <WorkflowPicker
3134
+ open={open}
3135
+ owner="mine"
3136
+ onPick={(workflow) => {
3137
+ setPicked(workflow);
3138
+ setOpen(false);
3139
+ }}
3140
+ onDismiss={() => setOpen(false)}
3141
+ />
3142
+ {picked && <p>{picked.name} — {picked.versionName}</p>}
3143
+ </>
3144
+ );
3145
+ }
3146
+ ```
3147
+
3148
+ A pick is a `PickedResource` — `{ modelId, modelVersionId, name, versionName,
3149
+ creatorUsername, baseModel, earlyAccessDeadline?, access, owner }`; `/ui`
3150
+ re-exports it and the picker's other data types (`ResourceItem`,
3151
+ `ResourceSource`, …). Close the picker yourself in
3152
+ **both** callbacks: `open` is controlled. The picker closes itself on Escape or
3153
+ a backdrop click, and re-opens on the next render if `open` still says `true`.
3154
+
3155
+ - 🔴 **`owner` is a UX hint, never proof of ownership.** It says which list the
3156
+ viewer was looking at, and your app's own code can set it to anything. A
3157
+ server that must know "this is the viewer's own workflow" checks that itself.
3158
+ - 🔴 **A "mine" list is only ever the server's.** The picker shows an error, not
3159
+ a list, when a `mine` search comes back without `metadata.owner: 'mine'` — a
3160
+ server older than the parameter ignores it and returns everybody's. It never
3161
+ falls back to the public `/api/v1/models`.
3162
+ - **Early-access and paid versions are listed, with a badge.** `access` on a
3163
+ pick is `'early-access'` (a timed paid-access gate, with its end date in
3164
+ `earlyAccessDeadline` when the server knows it), `'paid'` (a permanent gate)
3165
+ or `'open'`. The viewer may not be able to download a gated version later.
3166
+ - **Search is debounced** (300 ms), and a newer search cancels the one in
3167
+ flight. A busy catalog (`429` / `503`, rate limit 150 requests per 10 s per
3168
+ block) is retried after its `Retry-After`, twice, then offered as "Try again".
3169
+ - **An anonymous viewer** gets a sign-in message on the "mine" list; "All" works.
3170
+ An expired block token is not mistaken for that: on a `401` the picker mints a
3171
+ fresh token through `useBlockToken().refresh()` and retries once.
3172
+ - **Before `BLOCK_INIT`** the picker says it is not connected yet, and lists as
3173
+ soon as the host origin lands.
3174
+ - **Under the mock host** there is no catalog to call. Pass canned results from
3175
+ `/testing`:
3176
+
3177
+ ```tsx
3178
+ import { WorkflowPicker } from '@civitai/blocks-react/ui';
3179
+ import { fakeResourceSource } from '@civitai/blocks-react/testing';
3180
+
3181
+ const source = fakeResourceSource({
3182
+ mine: [
3183
+ {
3184
+ modelId: 1,
3185
+ name: 'My upscale chain',
3186
+ type: 'Workflows',
3187
+ creatorUsername: 'me',
3188
+ versions: [{ id: 11, name: 'v2', baseModel: 'SDXL 1.0' }],
3189
+ },
3190
+ ],
3191
+ });
3192
+
3193
+ export const Picker = () => <WorkflowPicker open source={source} onPick={console.log} />;
3194
+ ```
3195
+
3196
+ Under `dev:live` it works for real: the dev token can read `/blocks/models`.
3197
+
3115
3198
  ### `SettingsForm`
3116
3199
 
3117
3200
  The headless, manifest-driven settings form (its contract is intentionally
@@ -3205,6 +3288,7 @@ what went stale in [#334](https://github.com/civitai/civitai-app-starters/issues
3205
3288
  | `createMockHost` | A framework-agnostic fake of the embedding host — answers every `*_RESULT` message, with knobs for generation cost/latency/failure, Buzz balance, app + shared storage, consent, maturity. Returns a `MockHost`; call `.install()` and keep the returned teardown. **No network, no Buzz.** |
3206
3289
  | `readMockHostUrlOptions` | Reads the harness URL toggles (`?viewer` `?consent` `?fail` `?theme` `?pick` `?balance` `?latency` `?seed` …) into a `Partial<MockHostOptions>`. `Harness` applies it for you; call it directly only in a hand-rolled harness. |
3207
3290
  | `Harness` | The React wrapper: installs a `createMockHost` on mount, tears it down on unmount, and renders an optional on-screen outbound-message log. Takes every `MockHostOptions` field plus `applyUrlToggles` and `showLog`. |
3291
+ | `fakeResourceSource` | Canned results for `<WorkflowPicker>` (from `/ui`) or `<civitai-resource-picker>`: `fakeResourceSource({ mine, any })` filters by owner, type and query, echoes the owner it applied, and records each search on `.calls`. The mock host has no catalog, so pass it as the picker's `source`. **No network.** |
3208
3292
  | `installPopoverShim` | Stands in for the HTML popover API, which neither `jsdom` nor `happy-dom` implements at any version. Needed only if your own code calls `showPopover`/`hidePopover`/`togglePopover` or queries `:popover-open` — `@civitai/components`' own elements do not. Returns a `PopoverShimHandle`; inert (`installed: false`) in a real browser. **Read [Testing overlay elements](#testing-overlay-elements) first: it does not make trigger clicks work.** |
3209
3293
 
3210
3294
  <!-- TESTING-SURFACE:VALUES:END -->
@@ -3219,6 +3303,8 @@ type.
3219
3303
  ```text
3220
3304
  CannedPick
3221
3305
  CostSpec
3306
+ FakeResourceCanned
3307
+ FakeResourceSource
3222
3308
  HarnessProps
3223
3309
  ImageSpec
3224
3310
  MockBuzzBalance
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `fakeResourceSource` — canned results for `<WorkflowPicker>` /
3
+ * `<civitai-resource-picker>` where there is no catalog to call: the mock host,
4
+ * a unit test, a story. No network.
5
+ *
6
+ * It answers the way the real endpoint does where the picker can tell:
7
+ * it filters by `owner`, `types` and `query`, and echoes the `owner` it applied.
8
+ */
9
+ import type { ResourceItem, ResourceSearchParams, ResourceSource } from '@civitai/components/civitai-resource-picker';
10
+ /** What a {@link fakeResourceSource} lists. */
11
+ export interface FakeResourceCanned {
12
+ /** The viewer's own resources: what `owner=mine` lists. */
13
+ mine?: readonly ResourceItem[];
14
+ /**
15
+ * Everybody's: what `owner=any` lists. Defaults to `mine` plus nothing else,
16
+ * so a fake with only `mine` still shows them under "All".
17
+ */
18
+ any?: readonly ResourceItem[];
19
+ }
20
+ /** A {@link ResourceSource} that also records what it was asked. */
21
+ export interface FakeResourceSource extends ResourceSource {
22
+ /** Every search, oldest first. */
23
+ readonly calls: readonly ResourceSearchParams[];
24
+ }
25
+ /** Canned picker results. See {@link FakeResourceCanned}. */
26
+ export declare function fakeResourceSource(canned?: FakeResourceCanned): FakeResourceSource;
27
+ //# sourceMappingURL=fakeResourceSource.d.ts.map
@@ -0,0 +1,21 @@
1
+ /** Canned picker results. See {@link FakeResourceCanned}. */
2
+ export function fakeResourceSource(canned = {}) {
3
+ const calls = [];
4
+ const lists = {
5
+ mine: canned.mine ?? [],
6
+ any: canned.any ?? canned.mine ?? [],
7
+ };
8
+ return {
9
+ calls,
10
+ async search(params, signal) {
11
+ calls.push(params);
12
+ if (signal.aborted)
13
+ throw signal.reason ?? new DOMException('Aborted', 'AbortError');
14
+ const query = params.query?.trim().toLowerCase() ?? '';
15
+ const items = lists[params.owner].filter((item) => (params.types.length === 0 || params.types.includes(item.type)) &&
16
+ (query === '' || item.name.toLowerCase().includes(query)));
17
+ return { items: [...items], nextCursor: null, owner: params.owner };
18
+ },
19
+ };
20
+ }
21
+ //# sourceMappingURL=fakeResourceSource.js.map
@@ -547,8 +547,10 @@ export interface MockStorageScenario {
547
547
  * 🔴 AND THIS BUDGET IS COUNTED IN A DIFFERENT UNIT FROM THE HOST'S. The
548
548
  * mock sums WIRE bytes (`JSON.stringify` as UTF-8); the host sums STORED
549
549
  * bytes (`octet_length(value::jsonb::text)`), which is larger for every
550
- * container — up to ~1.5x for a long array. So this ceiling is up to half
551
- * again more generous than production's, and unlike the mock's other known
550
+ * container — ~1.5x for a long array of small integers, and measured up to
551
+ * 44.4x for a numeric-heavy one (an array of `1e308`). So this ceiling is
552
+ * more generous than production's, by a factor that depends on the data,
553
+ * and unlike the mock's other known
552
554
  * divergences that error is PERMISSIVE: a fixture that fits here can be
553
555
  * rejected live. civitai/civitai-app-starters#347. Size against
554
556
  * `getQuota()`, and treat a local pass as evidence, not proof.
@@ -3115,7 +3115,9 @@ export function createMockHost(options = {}) {
3115
3115
  // counts STORED bytes, `octet_length(value::jsonb::text)`.
3116
3116
  // `jsonb`'s canonical text inserts a space after every `:` and
3117
3117
  // every `,`, so stored > wire for every container — approaching
3118
- // 1.5x for a long array. This gate therefore ADMITS writes
3118
+ // 1.5x for a long array of small integers, and up to 44.4x for
3119
+ // a numeric-heavy one (`1e308` is stored as its 309 digits).
3120
+ // This gate therefore ADMITS writes
3119
3121
  // production rejects. Permissive: a local pass that is not
3120
3122
  // real, which is the direction that ships a bug.
3121
3123
  //
package/dist/testing.d.ts CHANGED
@@ -22,6 +22,7 @@ import { type MockHostOptions } from './internal/mockHost.js';
22
22
  export { __resetTransport as resetTransport };
23
23
  export { installPopoverShim, type PopoverShimHandle, type PopoverShimOptions, } from './internal/popoverShim.js';
24
24
  export { createMockHost, readMockHostUrlOptions, type MockHost, type MockHostOptions, type MockHostFailMode, type MockHostScenarioPatch, type MockGenerationScenario, type MockBuzzScenario, type MockBuzzBalance, type MockBuzzHandle, type MockStorageScenario, type MockSharedScenario, type MockSharedSeed, type MockCannedImageScan, type MockWorkflowStepInfo, type MockWorkflowQuoteFailure, type CostSpec, type ImageSpec, type CannedPick, } from './internal/mockHost.js';
25
+ export { fakeResourceSource, type FakeResourceCanned, type FakeResourceSource, } from './internal/fakeResourceSource.js';
25
26
  /**
26
27
  * Props for the dev {@link Harness}.
27
28
  */
package/dist/testing.js CHANGED
@@ -23,6 +23,7 @@ import { createMockHost, readMockHostUrlOptions, } from './internal/mockHost.js'
23
23
  export { __resetTransport as resetTransport };
24
24
  export { installPopoverShim, } from './internal/popoverShim.js';
25
25
  export { createMockHost, readMockHostUrlOptions, } from './internal/mockHost.js';
26
+ export { fakeResourceSource, } from './internal/fakeResourceSource.js';
26
27
  /**
27
28
  * What the harness chrome's `consent=` readout says, from the TWO INDEPENDENT
28
29
  * booleans that describe consent in the mock host.
@@ -0,0 +1,37 @@
1
+ import { type PickedResource, type ResourceOwner, type ResourceSource } from '@civitai/components/civitai-resource-picker';
2
+ /** The two model types a posted workflow can have; `ComfyWorkflows` is the retired one. */
3
+ export declare const WORKFLOW_PICKER_TYPES = "Workflows ComfyWorkflows";
4
+ export interface WorkflowPickerProps {
5
+ /**
6
+ * Shows the picker. Controlled: set it back to `false` in both callbacks. The
7
+ * picker closes itself on Escape or a backdrop click, and re-opens on the
8
+ * next render if this still says `true`.
9
+ */
10
+ open: boolean;
11
+ /**
12
+ * The viewer picked a version. 🔴 `picked.owner` is a UX hint, never proof the
13
+ * workflow is the viewer's own — check that server-side.
14
+ */
15
+ onPick: (picked: PickedResource) => void;
16
+ /** The viewer closed the picker without picking. */
17
+ onDismiss?: () => void;
18
+ /** Which list shows first. Defaults to `'any'`. */
19
+ owner?: ResourceOwner;
20
+ /** Defaults to `'Choose a workflow'`. */
21
+ heading?: string;
22
+ /**
23
+ * Where results come from. Defaults to the App Blocks catalog
24
+ * (`/api/v1/blocks/models`) through this block's host origin and token. Pass
25
+ * `fakeResourceSource(...)` from `@civitai/blocks-react/testing` under the
26
+ * mock host, which has no catalog to call.
27
+ */
28
+ source?: ResourceSource;
29
+ }
30
+ /**
31
+ * A workflow picker for a block: `<civitai-resource-picker>` listing
32
+ * `Workflows` and `ComfyWorkflows`, wired to {@link useHostOrigin} and
33
+ * {@link useBlockToken} so the viewer can pick one of their own posted
34
+ * workflows, or anyone's.
35
+ */
36
+ export declare function WorkflowPicker({ open, onPick, onDismiss, owner, heading, source, }: WorkflowPickerProps): React.JSX.Element;
37
+ //# sourceMappingURL=WorkflowPicker.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { createElement, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
2
+ import { blockTokenSource, defineCivitaiResourcePicker, ResourceSourceError, } from '@civitai/components/civitai-resource-picker';
3
+ import { useBlockToken } from '../hooks/useBlockToken.js';
4
+ import { useHostOrigin } from '../hooks/useHostOrigin.js';
5
+ /** The two model types a posted workflow can have; `ComfyWorkflows` is the retired one. */
6
+ export const WORKFLOW_PICKER_TYPES = 'Workflows ComfyWorkflows';
7
+ /**
8
+ * Answers every search while the host origin is unknown: an error the picker
9
+ * shows with a retry, never a silent wait. The picker searches again on its own
10
+ * once the real source replaces this one.
11
+ */
12
+ const NOT_CONNECTED = {
13
+ search: async () => {
14
+ throw new ResourceSourceError('failed', 'Not connected to Civitai yet.');
15
+ },
16
+ };
17
+ /**
18
+ * A workflow picker for a block: `<civitai-resource-picker>` listing
19
+ * `Workflows` and `ComfyWorkflows`, wired to {@link useHostOrigin} and
20
+ * {@link useBlockToken} so the viewer can pick one of their own posted
21
+ * workflows, or anyone's.
22
+ */
23
+ export function WorkflowPicker({ open, onPick, onDismiss, owner = 'any', heading = 'Choose a workflow', source, }) {
24
+ defineCivitaiResourcePicker();
25
+ const host = useHostOrigin();
26
+ const { raw, refresh } = useBlockToken();
27
+ // Read at each request, so a refreshed token is the one sent.
28
+ const token = useRef({ raw, refresh });
29
+ token.current = { raw, refresh };
30
+ const live = useMemo(() => host
31
+ ? blockTokenSource({
32
+ origin: host,
33
+ // After a 401: mint a new token (an expired one is routine in a
34
+ // tab that slept). A failed mint returns the old token, which ends
35
+ // the search as `unauthorized`.
36
+ getToken: (options) => options?.fresh
37
+ ? token.current.refresh().then((fresh) => fresh.raw, () => token.current.raw)
38
+ : token.current.raw,
39
+ })
40
+ : NOT_CONNECTED, [host]);
41
+ const ref = useRef(null);
42
+ const handlers = useRef({ onPick, onDismiss });
43
+ handlers.current = { onPick, onDismiss };
44
+ // Properties, not attributes: React 18 would stringify the source.
45
+ useLayoutEffect(() => {
46
+ if (ref.current)
47
+ ref.current.source = source ?? live;
48
+ }, [source, live]);
49
+ // No dependency list on purpose: the element closes itself on Escape or a
50
+ // backdrop click, so it can disagree with `open` without the prop changing.
51
+ // Every render puts it back in line, which is what lets a consumer that
52
+ // never handled `onDismiss` open it again.
53
+ useLayoutEffect(() => {
54
+ if (ref.current && ref.current.open !== open)
55
+ ref.current.open = open;
56
+ });
57
+ useEffect(() => {
58
+ const el = ref.current;
59
+ if (!el)
60
+ return;
61
+ const pick = (event) => handlers.current.onPick(event.detail);
62
+ const dismiss = () => handlers.current.onDismiss?.();
63
+ el.addEventListener('pick', pick);
64
+ el.addEventListener('dismiss', dismiss);
65
+ return () => {
66
+ el.removeEventListener('pick', pick);
67
+ el.removeEventListener('dismiss', dismiss);
68
+ };
69
+ }, []);
70
+ return createElement('civitai-resource-picker', {
71
+ ref,
72
+ types: WORKFLOW_PICKER_TYPES,
73
+ noun: 'workflow',
74
+ owner,
75
+ heading,
76
+ });
77
+ }
78
+ //# sourceMappingURL=WorkflowPicker.js.map
@@ -58,4 +58,7 @@ export { Collapse } from './Collapse.js';
58
58
  export type { CollapseProps } from './Collapse.js';
59
59
  export { SegmentedControl } from './SegmentedControl.js';
60
60
  export type { SegmentedControlProps, SegmentedControlItem, SegmentedControlSize, } from './SegmentedControl.js';
61
+ export { WorkflowPicker, WORKFLOW_PICKER_TYPES } from './WorkflowPicker.js';
62
+ export type { WorkflowPickerProps } from './WorkflowPicker.js';
63
+ export type { PickedResource, ResourceAccess, ResourceItem, ResourceOwner, ResourcePage, ResourceSearchParams, ResourceSource, ResourceVersion, } from '@civitai/components/civitai-resource-picker';
61
64
  //# sourceMappingURL=index.d.ts.map
package/dist/ui/index.js CHANGED
@@ -43,4 +43,5 @@ export { NumberInput } from './NumberInput.js';
43
43
  export { Select } from './Select.js';
44
44
  export { Collapse } from './Collapse.js';
45
45
  export { SegmentedControl } from './SegmentedControl.js';
46
+ export { WorkflowPicker, WORKFLOW_PICKER_TYPES } from './WorkflowPicker.js';
46
47
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
1
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
2
- export declare const VERSION = "0.73.1";
2
+ export declare const VERSION = "0.74.0";
3
3
  //# sourceMappingURL=version.generated.d.ts.map
@@ -1,4 +1,4 @@
1
1
  /* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
2
2
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
3
- export const VERSION = "0.73.1";
3
+ export const VERSION = "0.74.0";
4
4
  //# sourceMappingURL=version.generated.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.73.1",
3
+ "version": "0.74.0",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "node": ">=20"
38
38
  },
39
39
  "dependencies": {
40
- "@civitai/components": "^0.10.0",
40
+ "@civitai/components": "^0.11.0",
41
41
  "@civitai/theme": "^0.5.2"
42
42
  },
43
43
  "comment-peerDependencies": "This floor is DERIVED, not chosen — the lowest published app-sdk that exports every symbol this package imports. Do not edit it without reading ./PEER_FLOOR.md (in-repo, unpublished) and tests/guards/blocks-react-peer-floor.test.mjs.",
@@ -58,7 +58,7 @@
58
58
  "typescript": "^5.9.2",
59
59
  "vite": "^8.0.14",
60
60
  "vitest": "^4.1.11",
61
- "@civitai/app-sdk": "^0.70.1"
61
+ "@civitai/app-sdk": "^0.71.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"