@rightkit/onboarding 0.3.3 → 0.5.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
@@ -13,7 +13,7 @@ First-run onboarding framework for Right Suite apps. The legal agreement gate co
13
13
 
14
14
  | Import | Contents |
15
15
  |---|---|
16
- | `@rightkit/onboarding` | `Onboarding`, `isLegalAccepted`, `onboardingStateKey`, `licenseStep`, `modelsStep`, `permissionsStep`; types from `types.ts`, `ModelsStepOptions` |
16
+ | `@rightkit/onboarding` | `Onboarding`, `isLegalAccepted`, `onboardingStateKey`, `licenseStep`, `modelsStep`, `permissionsStep`, `signInStep` (Google/Apple/OAuth buttons that call the shared `rightkit-oauth` owner; copy says "Opens your browser"; never an in-app webview, Orthic-Labs/rightkit#279); types from `types.ts`, `ModelsStepOptions` |
17
17
  | `@rightkit/onboarding/onboarding.css` | Stylesheet |
18
18
 
19
19
  ## Peer dependencies
@@ -43,6 +43,8 @@ import "@rightkit/onboarding/onboarding.css";
43
43
  />
44
44
  ```
45
45
 
46
+ Free or open-source apps without a legal gate pass `legalMode="none"` (no legal step, `legal` not needed, `appName` for the rail) or `legalMode="notice"` (no gate or acceptance; a first Notice step lists `legal.documents` as links). The default is `"gate"`.
47
+
46
48
  Acceptance is stored as `legalAcceptance: { version, documentsHash, acceptedAt, eligibilityBasis }`. An app that already keeps its own acceptance receipt can pass `legalReceipt` (`read()` and `write(record)`) and keep that store as the source of truth. Steps can set `canContinue(context)`.
47
49
 
48
50
  ## When the legal gate re-prompts
@@ -2,4 +2,4 @@ import type { OnboardingProps, PersistedOnboardingState } from "./types.js";
2
2
  /** True when the stored acceptance satisfies the host's requirement. */
3
3
  export declare function isLegalAccepted(state: PersistedOnboardingState, requiredVersion?: string, custom?: (state: PersistedOnboardingState) => boolean, requiredDocumentsHash?: string): boolean;
4
4
  export declare function onboardingStateKey(): string;
5
- export declare function Onboarding({ settings, legal, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt }: OnboardingProps): import("react").JSX.Element;
5
+ export declare function Onboarding({ settings, legal, legalMode, appName, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt }: OnboardingProps): import("react").JSX.Element | null;
@@ -79,7 +79,17 @@ function TitleStrip({ titlebar, dragRegion }) {
79
79
  return null;
80
80
  return _jsx("div", { className: "rk-onboarding__titlebar", "data-testid": "onboarding-titlebar", ...(dragRegion ? { "data-tauri-drag-region": "" } : {}), children: titlebar });
81
81
  }
82
- export function Onboarding({ settings, legal, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt }) {
82
+ function NoticeList({ documents }) {
83
+ if (!documents.length)
84
+ return _jsx("p", { className: "rk-onboarding__muted", children: "No notices to show." });
85
+ return _jsx("ul", { className: "rk-onboarding__notices", children: documents.map((d) => _jsx("li", { children: d.onOpen ? _jsx("button", { type: "button", onClick: d.onOpen, children: d.title }) : d.url ? _jsx("a", { href: d.url, target: "_blank", rel: "noreferrer", children: d.title }) : d.title }, d.title)) });
86
+ }
87
+ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt }) {
88
+ if (legalMode === "gate" && !legal)
89
+ throw new Error("Onboarding: `legal` is required when legalMode is \"gate\"");
90
+ const gated = legalMode === "gate";
91
+ /** Leading steps the user cannot navigate to: the Agreement stand-in in gate mode. */
92
+ const lead = gated ? 1 : 0;
83
93
  const [state, setState] = useState(null);
84
94
  const [saving, setSaving] = useState(false);
85
95
  const [error, setError] = useState(null);
@@ -144,18 +154,19 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
144
154
  return () => { live = false; };
145
155
  }, [settings, legalReceipt]);
146
156
  const builtins = useMemo(() => [
147
- { id: "legal", label: "Agreement", title: "Review agreement", skippable: false, render: () => _jsx("p", { className: "rk-onboarding__muted", children: "Agreement accepted." }) },
157
+ ...(gated ? [{ id: "legal", label: "Agreement", title: "Review agreement", skippable: false, render: () => _jsx("p", { className: "rk-onboarding__muted", children: "Agreement accepted." }) }] : []),
158
+ ...(legalMode === "notice" ? [{ id: "notice", label: "Notice", title: "Notices", skippable: false, render: () => _jsx(NoticeList, { documents: legal?.documents ?? [] }) }] : []),
148
159
  ...(permissions ? [permissionsStep(permissions)] : []),
149
160
  ...(models ? [modelsStep(models, { required: requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, intro: modelsIntro, startGuard: modelsStarted })] : []),
150
161
  ...(license ? [licenseStep(license)] : []),
151
162
  ...steps,
152
- ], [license, models, permissions, steps, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro]);
163
+ ], [gated, legalMode, legal?.documents, license, models, permissions, steps, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro]);
153
164
  const activeState = state ?? INITIAL;
154
- const accepted = isLegalAccepted(activeState, requiredVersion, isAccepted, requiredDocumentsHash);
165
+ const accepted = !gated || isLegalAccepted(activeState, requiredVersion, isAccepted, requiredDocumentsHash);
155
166
  const needsGate = state !== null && !accepted;
156
167
  // The agreement gate does not depend on the saved step: until it is accepted, the active step is the first.
157
168
  // Once accepted, the Agreement step (index 0) is never shown again when other steps exist.
158
- const index = !accepted ? 0 : builtins.length > 1 ? Math.max(1, Math.min(activeState.currentStep, builtins.length - 1)) : 0;
169
+ const index = !accepted ? 0 : builtins.length > lead ? Math.max(lead, Math.min(activeState.currentStep, builtins.length - 1)) : 0;
159
170
  const active = builtins[index];
160
171
  const activeId = active?.id ?? "";
161
172
  const setReady = useCallback((ready) => setReadyMap((m) => m[activeId] === ready ? m : { ...m, [activeId]: ready }), [activeId]);
@@ -198,11 +209,15 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
198
209
  };
199
210
  if (error && !state)
200
211
  return _jsxs("div", { className: cx("rk-onboarding", className), children: [_jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }), _jsx(Button, { onClick: () => window.location.reload(), children: "Retry" })] });
212
+ if (state && !active)
213
+ return _jsxs("main", { className: cx("rk-onboarding", className), "data-testid": "onboarding", children: [_jsx("p", { className: "rk-onboarding__muted", children: "Nothing to set up." }), _jsx(Button, { variant: "primary", onClick: () => void runCallback(onComplete), children: "Finish setup" })] });
201
214
  if (!state || !active)
202
215
  return _jsx("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: _jsx("p", { className: "rk-onboarding__muted", children: "Loading setup\u2026" }) });
203
216
  if (needsGate) {
204
217
  if (!gateCleared)
205
218
  return _jsxs("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: legalDragRegion }), _jsx("p", { className: "rk-onboarding__muted", children: "Loading setup\u2026" })] });
219
+ if (!legal)
220
+ return null;
206
221
  const reacceptance = activeState.legalAccepted || activeState.legalAcceptance !== undefined;
207
222
  return _jsxs("div", { className: cx("rk-onboarding rk-onboarding--legal", className), "data-testid": "onboarding-legal", "data-variant": reacceptance ? "re-acceptance" : "first-run", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: legalDragRegion }), _jsx(LegalGate, { ...legal, variant: reacceptance ? "re-acceptance" : "first-run", onQuit: () => void legal.onQuit?.(), onAgree: async ({ eligibilityBasis }) => {
208
223
  const legalAcceptance = { version: requiredVersion ?? "1", documentsHash: documentsHash ?? requiredDocumentsHash ?? await hashDocuments(legal.documents), acceptedAt: new Date().toISOString(), eligibilityBasis };
@@ -222,6 +237,6 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
222
237
  } })] });
223
238
  }
224
239
  const body = active.render(context);
225
- return _jsxs("main", { className: cx("rk-onboarding", className), "data-testid": "onboarding", "data-step-id": active.id, children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: dragRegion }), _jsxs("aside", { className: "rk-onboarding__rail", children: [_jsx("div", { className: "rk-onboarding__brand", children: legal.appName }), _jsx("nav", { "aria-label": "Setup progress", children: builtins.map((step, i) => _jsxs("button", { type: "button", className: cx("rk-onboarding__step", i === index && "is-active", activeState.completed.includes(step.id) && "is-done"), "aria-current": i === index ? "step" : undefined, disabled: i === 0 && builtins.length > 1, onClick: () => { if (i > 0 && (i <= index || canContinue))
226
- void save({ currentStep: i }); }, children: [_jsx("span", { children: activeState.completed.includes(step.id) ? "✓" : i + 1 }), step.label] }, step.id)) })] }), _jsxs("section", { className: "rk-onboarding__main", children: [_jsxs("header", { children: [_jsxs("p", { className: "rk-onboarding__eyebrow", children: ["Step ", index + 1, " of ", builtins.length] }), _jsx("h1", { children: active.title }), active.description ? _jsx("p", { className: "rk-onboarding__description", children: active.description }) : null] }), _jsx("div", { className: "rk-onboarding__content", children: body }), error ? _jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }) : null, _jsxs("footer", { children: [allowSkip ? _jsx(Button, { variant: "ghost", disabled: saving, onClick: () => { void save({ currentStep: index, skipped: true }).then((ok) => ok ? runCallback(onSkip) : undefined); }, children: "Set up later" }) : null, _jsx("span", { className: "rk-onboarding__hint", children: !canContinue && active.blockedHint ? active.blockedHint : null }), _jsx(Button, { variant: "secondary", disabled: index <= 1 || saving, onClick: () => void save({ currentStep: index - 1 }), children: "Back" }), _jsx(Button, { variant: "primary", disabled: saving || !canContinue, onClick: () => void markAndMove(index + 1, active.id), children: index === builtins.length - 1 ? "Finish setup" : "Continue" })] })] })] });
240
+ return _jsxs("main", { className: cx("rk-onboarding", className), "data-testid": "onboarding", "data-step-id": active.id, children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: dragRegion }), _jsxs("aside", { className: "rk-onboarding__rail", children: [_jsx("div", { className: "rk-onboarding__brand", children: appName ?? legal?.appName }), _jsx("nav", { "aria-label": "Setup progress", children: builtins.map((step, i) => _jsxs("button", { type: "button", className: cx("rk-onboarding__step", i === index && "is-active", activeState.completed.includes(step.id) && "is-done"), "aria-current": i === index ? "step" : undefined, disabled: gated && i === 0 && builtins.length > 1, onClick: () => { if (i >= lead && (i <= index || canContinue))
241
+ void save({ currentStep: i }); }, children: [_jsx("span", { children: activeState.completed.includes(step.id) ? "✓" : i + 1 }), step.label] }, step.id)) })] }), _jsxs("section", { className: "rk-onboarding__main", children: [_jsxs("header", { children: [_jsxs("p", { className: "rk-onboarding__eyebrow", children: ["Step ", index + 1, " of ", builtins.length] }), _jsx("h1", { children: active.title }), active.description ? _jsx("p", { className: "rk-onboarding__description", children: active.description }) : null] }), _jsx("div", { className: "rk-onboarding__content", children: body }), error ? _jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }) : null, _jsxs("footer", { children: [allowSkip ? _jsx(Button, { variant: "ghost", disabled: saving, onClick: () => { void save({ currentStep: index, skipped: true }).then((ok) => ok ? runCallback(onSkip) : undefined); }, children: "Set up later" }) : null, _jsx("span", { className: "rk-onboarding__hint", children: !canContinue && active.blockedHint ? active.blockedHint : null }), _jsx(Button, { variant: "secondary", disabled: index <= lead || saving, onClick: () => void save({ currentStep: index - 1 }), children: "Back" }), _jsx(Button, { variant: "primary", disabled: saving || !canContinue, onClick: () => void markAndMove(index + 1, active.id), children: index === builtins.length - 1 ? "Finish setup" : "Continue" })] })] })] });
227
242
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { Onboarding, isLegalAccepted, onboardingStateKey } from "./Onboarding.js";
2
- export { licenseStep, modelsStep, permissionsStep } from "./steps.js";
2
+ export { licenseStep, modelsStep, permissionsStep, signInStep } from "./steps.js";
3
3
  export type { ModelsStepOptions } from "./steps.js";
4
4
  export type * from "./types.js";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { Onboarding, isLegalAccepted, onboardingStateKey } from "./Onboarding.js";
2
- export { licenseStep, modelsStep, permissionsStep } from "./steps.js";
2
+ export { licenseStep, modelsStep, permissionsStep, signInStep } from "./steps.js";
package/dist/steps.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import type { LicenseBackend, ModelsAutoStart, ModelsBackend, OnboardingStep, PermissionsBackend } from "./types.js";
2
+ import type { SignInBackend, LicenseBackend, ModelsAutoStart, ModelsBackend, OnboardingStep, PermissionsBackend } from "./types.js";
3
3
  export declare function permissionsStep(backend: PermissionsBackend, options?: {
4
4
  label?: string;
5
5
  title?: string;
@@ -26,3 +26,9 @@ export declare function licenseStep(backend: LicenseBackend, options?: {
26
26
  title?: string;
27
27
  description?: string;
28
28
  }): OnboardingStep;
29
+ /** Sign-in step. Every button hands off to the shared owner and opens the user's default browser; no in-app webview. */
30
+ export declare function signInStep(backend: SignInBackend, options?: {
31
+ label?: string;
32
+ title?: string;
33
+ description?: string;
34
+ }): OnboardingStep;
package/dist/steps.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { Button, Input, Badge } from "@rightkit/react-shell/react";
4
4
  function ProgressBar({ value, label }) {
@@ -113,3 +113,28 @@ function LicenseBody({ backend }) {
113
113
  export function licenseStep(backend, options = {}) {
114
114
  return { id: "license", label: options.label ?? "Licence", title: options.title ?? "Choose how you’ll use this app", description: options.description ?? "Activate a licence, start a trial, or continue with the free plan.", skippable: true, render: () => _jsx(LicenseBody, { backend: backend }) };
115
115
  }
116
+ function SignInBody({ backend }) {
117
+ const [account, setAccount] = useState({ signedIn: false });
118
+ const [busy, setBusy] = useState(null);
119
+ const [error, setError] = useState(null);
120
+ useEffect(() => { let live = true; backend.load().then((a) => { if (live)
121
+ setAccount(a); }, () => undefined); return () => { live = false; }; }, [backend]);
122
+ const start = async (provider) => {
123
+ setBusy(provider);
124
+ setError(null);
125
+ try {
126
+ setAccount(await backend.start(provider));
127
+ }
128
+ catch (e) {
129
+ setError(e instanceof Error ? e.message : "Sign-in did not finish.");
130
+ }
131
+ finally {
132
+ setBusy(null);
133
+ }
134
+ };
135
+ return _jsxs("div", { className: "rk-onboarding__signin", children: [account.signedIn ? _jsxs("p", { children: ["Signed in", account.account ? _jsxs(_Fragment, { children: [" as ", _jsx("strong", { children: account.account })] }) : null, "."] }) : null, _jsx("div", { className: "rk-onboarding__row-actions", children: backend.providers.map((p) => _jsx(Button, { size: "sm", variant: "secondary", disabled: busy !== null, onClick: () => void start(p.id), children: busy === p.id ? "Waiting for your browser…" : p.label ?? `Sign in with ${p.id === "google" ? "Google" : p.id === "apple" ? "Apple" : "your account"}` }, p.id)) }), _jsx("p", { className: "rk-onboarding__hint", children: _jsx("small", { children: "Opens your browser. You stay signed in to the accounts you already use there." }) }), error ? _jsx("p", { role: "alert", children: error }) : null] });
136
+ }
137
+ /** Sign-in step. Every button hands off to the shared owner and opens the user's default browser; no in-app webview. */
138
+ export function signInStep(backend, options = {}) {
139
+ return { id: "sign-in", label: options.label ?? "Sign in", title: options.title ?? "Sign in", description: options.description ?? "Sign-in opens your default browser, so you stay signed in to the accounts you already use there.", skippable: true, render: () => _jsx(SignInBody, { backend: backend }) };
140
+ }
package/dist/types.d.ts CHANGED
@@ -57,6 +57,28 @@ export interface LicenseBackend {
57
57
  purchaseUrl?: string;
58
58
  openExternal?: (url: string) => void | Promise<void>;
59
59
  }
60
+ export type SignInProvider = "google" | "apple" | "oauth";
61
+ /**
62
+ * Sign-in owned by `rightkit-oauth`. Every provider opens the user's DEFAULT browser (never an in-app webview, so existing
63
+ * browser sessions are kept); `start` must call the shared owner (`OAuthClient::sign_in_with_browser` through the app's
64
+ * command), not open a window of its own. Orthic-Labs/rightkit#279.
65
+ */
66
+ export interface SignInBackend {
67
+ providers: readonly {
68
+ id: SignInProvider;
69
+ label?: string;
70
+ }[];
71
+ /** Account already signed in, if any. */
72
+ load(): Promise<{
73
+ signedIn: boolean;
74
+ account?: string;
75
+ }>;
76
+ /** Open the default browser for `provider` and resolve when the sign-in finishes (or reject / throw on failure or cancel). */
77
+ start(provider: SignInProvider): Promise<{
78
+ signedIn: boolean;
79
+ account?: string;
80
+ }>;
81
+ }
60
82
  export type EligibilityBasis = "individual" | "enterprise";
61
83
  /** Full record of one legal acceptance, persisted so version changes can re-prompt. */
62
84
  export interface LegalAcceptanceRecord {
@@ -94,6 +116,7 @@ export interface OnboardingStepContext {
94
116
  permissions?: PermissionsBackend;
95
117
  models?: ModelsBackend;
96
118
  license?: LicenseBackend;
119
+ signIn?: SignInBackend;
97
120
  /** Readiness reported by steps via `setReady`, keyed by step id. */
98
121
  ready: Readonly<Record<string, boolean>>;
99
122
  /** Reports whether the active step is ready (used by `canContinue`). Stable per step. */
@@ -114,9 +137,21 @@ export interface OnboardingStep {
114
137
  export interface LegalStepConfig extends Omit<LegalGateProps, "variant" | "onAgree" | "onQuit"> {
115
138
  onQuit?: () => void;
116
139
  }
140
+ /**
141
+ * How legal documents take part in onboarding.
142
+ * - `gate` (default): the blocking agreement gate comes first and is recorded (needs `legal`).
143
+ * - `notice`: no gate and no acceptance. A first "Notice" step lists the `legal` documents as links; Continue moves on (free or open-source apps that still hold their notice).
144
+ * - `none`: no legal step at all; `legal` is not needed.
145
+ */
146
+ export type LegalMode = "gate" | "notice" | "none";
117
147
  export interface OnboardingProps {
118
148
  settings: SettingsBackend;
119
- legal: LegalStepConfig;
149
+ /** Required when `legalMode` is `gate` (default). Optional for `notice` (the documents shown) and `none`. */
150
+ legal?: LegalStepConfig;
151
+ /** Default `gate`. See `LegalMode`. */
152
+ legalMode?: LegalMode;
153
+ /** Name shown in the step rail. Default: `legal.appName`. */
154
+ appName?: string;
120
155
  /** App-specific steps are appended after built-in steps, in supplied order. */
121
156
  steps?: readonly OnboardingStep[];
122
157
  permissions?: PermissionsBackend;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/onboarding",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "First-run onboarding framework: ordered steps, progress, skip/resume, legal gate first, permissions, model download and licence/trial steps.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -29,20 +29,21 @@
29
29
  "react": ">=18 <21"
30
30
  },
31
31
  "devDependencies": {
32
- "@rightkit/react-shell": "0.6.4",
33
- "@rightkit/legal-ui": "0.2.2",
34
- "@rightkit/qa-chrome": "0.1.6",
35
- "@testing-library/jest-dom": "^6.6.3",
36
- "@testing-library/react": "^16.1.0",
37
- "@testing-library/user-event": "^14.5.2",
38
- "@types/react": "^19.0.0",
39
- "@types/react-dom": "^19.0.0",
40
- "jsdom": "^25.0.1",
41
- "react": "^19.0.0",
42
- "react-dom": "^19.0.0",
43
- "typescript": "^5.7.0",
44
- "vite": "^6.0.0",
45
- "vitest": "^2.1.8"
32
+ "@rightkit/react-shell": "0.6.8",
33
+ "@rightkit/legal-ui": "0.3.0",
34
+ "@rightkit/qa-chrome": "0.1.8",
35
+ "@testing-library/jest-dom": "^7.0.1",
36
+ "@testing-library/dom": "^10.4.2",
37
+ "@testing-library/react": "^16.3.3",
38
+ "@testing-library/user-event": "^14.6.8",
39
+ "@types/react": "^19.3.0",
40
+ "@types/react-dom": "^19.3.0",
41
+ "jsdom": "^30.1.3",
42
+ "react": "^19.3.1",
43
+ "react-dom": "^19.3.1",
44
+ "typescript": "^7.0.2",
45
+ "vite": "^8.3.4",
46
+ "vitest": "^5.0.5"
46
47
  },
47
48
  "publishConfig": {
48
49
  "registry": "https://registry.npmjs.org",