castle-web-sdk 0.4.3 → 0.4.5

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/castle.d.ts CHANGED
@@ -2,7 +2,9 @@ export { isEdit } from "./context";
2
2
  export { CastleError } from "./errors";
3
3
  export { Leaderboard } from "./leaderboard";
4
4
  export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardSort, } from "./leaderboard";
5
- export { CARD_RATIO, initCard, setup, writeFile } from "./runtime";
5
+ export { Pass } from "./passes";
6
+ export type { CastlePassApi, PassOfferResult, PassOfferStatus, } from "./passes";
7
+ export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
6
8
  export { SharedStorage, Storage } from "./storage";
7
9
  export { Time } from "./time";
8
10
  export type { CastleClockZone, CastleDateParts, CastleTimeApi } from "./time";
package/dist/castle.js CHANGED
@@ -2,7 +2,8 @@
2
2
  export { isEdit } from "./context";
3
3
  export { CastleError } from "./errors";
4
4
  export { Leaderboard } from "./leaderboard";
5
- export { CARD_RATIO, initCard, setup, writeFile } from "./runtime";
5
+ export { Pass } from "./passes";
6
+ export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
6
7
  export { SharedStorage, Storage } from "./storage";
7
8
  export { Time } from "./time";
8
9
  export { User } from "./user";
@@ -0,0 +1,117 @@
1
+ import type { Json } from "./types";
2
+ export declare const CASTLE_SDK_PROTOCOL = 1;
3
+ export type StorageBlob = Record<string, string>;
4
+ export type SharedScope = "deck" | "user";
5
+ export interface StorageUpdate {
6
+ key: string;
7
+ value: string | null;
8
+ }
9
+ export interface RawLeaderboardEntry {
10
+ place?: string | number | null;
11
+ score?: string | number | null;
12
+ user?: {
13
+ userId?: string | null;
14
+ username?: string | null;
15
+ } | null;
16
+ }
17
+ export interface RawLeaderboard {
18
+ list?: RawLeaderboardEntry[] | null;
19
+ yourScore?: {
20
+ score?: string | number | null;
21
+ } | null;
22
+ }
23
+ export type PassOfferStatus = "purchased" | "alreadyOwned" | "cancelled" | "unavailable";
24
+ export interface PassOfferResult {
25
+ status: PassOfferStatus;
26
+ }
27
+ export interface CommandParams {
28
+ "deckStorage.load": Record<string, never>;
29
+ "deckStorage.update": {
30
+ updates: StorageUpdate[];
31
+ };
32
+ "sharedDeckStorage.load": {
33
+ scope: SharedScope;
34
+ userId?: string | null;
35
+ keys: string[];
36
+ };
37
+ "sharedDeckStorage.update": {
38
+ scope: SharedScope;
39
+ updates: StorageUpdate[];
40
+ };
41
+ "leaderboard.fetch": {
42
+ variable: string;
43
+ type: "high" | "low";
44
+ scope?: string | null;
45
+ score?: number | null;
46
+ };
47
+ "leaderboard.save": {
48
+ variable: string;
49
+ score: number;
50
+ scope?: string | null;
51
+ };
52
+ "user.getCurrent": Record<string, never>;
53
+ "time.getServerTime": Record<string, never>;
54
+ "pass.has": {
55
+ passId: string;
56
+ };
57
+ "pass.offer": {
58
+ passId: string;
59
+ };
60
+ }
61
+ export interface CommandResult {
62
+ "deckStorage.load": {
63
+ blob: StorageBlob;
64
+ };
65
+ "deckStorage.update": {
66
+ blob: StorageBlob;
67
+ };
68
+ "sharedDeckStorage.load": {
69
+ blob: StorageBlob;
70
+ };
71
+ "sharedDeckStorage.update": {
72
+ ok: true;
73
+ };
74
+ "leaderboard.fetch": {
75
+ leaderboard: RawLeaderboard;
76
+ currentUserId: string | null;
77
+ };
78
+ "leaderboard.save": {
79
+ ok: true;
80
+ };
81
+ "user.getCurrent": {
82
+ user: {
83
+ userId: string;
84
+ username: string;
85
+ } | null;
86
+ };
87
+ "time.getServerTime": {
88
+ timestamp: number;
89
+ timezoneOffset: number;
90
+ castleEpochData: Json;
91
+ };
92
+ "pass.has": {
93
+ hasPass: boolean;
94
+ };
95
+ "pass.offer": PassOfferResult;
96
+ }
97
+ export type CommandName = keyof CommandParams;
98
+ export interface SerializedCommandError {
99
+ code: string;
100
+ message: string;
101
+ command?: string;
102
+ extensions?: Record<string, unknown>;
103
+ }
104
+ export interface CommandRequestEnvelope {
105
+ castleSdk: typeof CASTLE_SDK_PROTOCOL;
106
+ requestId: string;
107
+ command: CommandName;
108
+ params: unknown;
109
+ }
110
+ export interface CommandResponseEnvelope {
111
+ castleSdk: typeof CASTLE_SDK_PROTOCOL;
112
+ requestId: string;
113
+ ok: boolean;
114
+ data?: unknown;
115
+ error?: SerializedCommandError;
116
+ }
117
+ export declare function isResponseEnvelope(value: unknown): value is CommandResponseEnvelope;
@@ -0,0 +1,16 @@
1
+ // The SDK↔host command contract. This is the single source of truth for the
2
+ // wire protocol shared by the deck-side command-poster (`transport.ts`) and the
3
+ // host-side executor (`host.ts`). It carries ONLY names and types — no GraphQL
4
+ // query strings and no auth — so importing it into a deck bundle reveals
5
+ // nothing privileged.
6
+ // Marker + protocol version. Doubles as a discriminator so host messages can't
7
+ // be confused with embed.js `castlexyz:` strings or RN console messages.
8
+ export const CASTLE_SDK_PROTOCOL = 1;
9
+ export function isResponseEnvelope(value) {
10
+ if (typeof value !== "object" || value === null)
11
+ return false;
12
+ const record = value;
13
+ return (record.castleSdk === CASTLE_SDK_PROTOCOL &&
14
+ typeof record.requestId === "string" &&
15
+ typeof record.ok === "boolean");
16
+ }
package/dist/context.d.ts CHANGED
@@ -1,21 +1,8 @@
1
- export interface CastleDeckContext {
2
- deckId?: string | null;
3
- cardId?: string | null;
4
- sessionId?: string | null;
5
- }
6
- interface CastleEmbedAuth {
7
- token?: string | null;
8
- userId?: string | null;
9
- }
1
+ export type CastleHost = "web" | "mobile" | "dev";
10
2
  export interface CastleEmbed {
11
3
  edit?: boolean;
12
4
  feed?: boolean;
13
- auth?: CastleEmbedAuth;
14
- deck?: CastleDeckContext;
15
- deckId?: string;
16
- cardId?: string;
17
- sessionId?: string | null;
18
- graphqlEndpoint?: string;
5
+ host?: CastleHost;
19
6
  }
20
7
  declare global {
21
8
  interface Window {
@@ -24,7 +11,3 @@ declare global {
24
11
  }
25
12
  export declare function getCastleEmbed(): CastleEmbed | undefined;
26
13
  export declare function isEdit(): boolean;
27
- export declare function configureDeckContext(context: CastleDeckContext): void;
28
- export declare function getDeckContext(): Promise<CastleDeckContext>;
29
- export declare function requireDeckId(operation?: string): Promise<string>;
30
- export {};
package/dist/context.js CHANGED
@@ -1,7 +1,3 @@
1
- import { CastleError } from "./errors";
2
- let configuredDeckContext = {};
3
- let cachedDeckContext = null;
4
- let cachedDeckContextPromise = null;
5
1
  export function getCastleEmbed() {
6
2
  return typeof window === "undefined" ? undefined : window.CastleEmbed;
7
3
  }
@@ -17,70 +13,3 @@ export function isEdit() {
17
13
  }
18
14
  return !!getCastleEmbed()?.edit;
19
15
  }
20
- export function configureDeckContext(context) {
21
- configuredDeckContext = cleanDeckContext(context);
22
- cachedDeckContext = null;
23
- cachedDeckContextPromise = null;
24
- }
25
- export async function getDeckContext() {
26
- if (cachedDeckContext)
27
- return cachedDeckContext;
28
- cachedDeckContextPromise ??= resolveDeckContext();
29
- return cachedDeckContextPromise;
30
- }
31
- export async function requireDeckId(operation = "Castle API request") {
32
- const context = await getDeckContext();
33
- if (!context.deckId) {
34
- throw new CastleError({
35
- code: "MISSING_DECK_ID",
36
- message: "This Castle API needs a deck id.",
37
- operation,
38
- });
39
- }
40
- return context.deckId;
41
- }
42
- async function resolveDeckContext() {
43
- const embedded = embeddedDeckContext();
44
- const local = await localDeckContext();
45
- const context = mergeDeckContexts(configuredDeckContext, embedded, local);
46
- cachedDeckContext = context;
47
- return context;
48
- }
49
- function embeddedDeckContext() {
50
- const embed = getCastleEmbed();
51
- return cleanDeckContext({
52
- deckId: embed?.deck?.deckId ?? embed?.deckId,
53
- cardId: embed?.deck?.cardId ?? embed?.cardId,
54
- sessionId: embed?.deck?.sessionId ?? embed?.sessionId,
55
- });
56
- }
57
- async function localDeckContext() {
58
- try {
59
- const res = await fetch("/__castle/context");
60
- if (!res.ok)
61
- return {};
62
- return cleanDeckContext((await res.json()));
63
- }
64
- catch {
65
- return {};
66
- }
67
- }
68
- function cleanDeckContext(context) {
69
- return {
70
- deckId: stringOrNull(context.deckId),
71
- cardId: stringOrNull(context.cardId),
72
- sessionId: stringOrNull(context.sessionId),
73
- };
74
- }
75
- function mergeDeckContexts(...contexts) {
76
- return cleanDeckContext({
77
- deckId: contexts.find((context) => context.deckId)?.deckId,
78
- cardId: contexts.find((context) => context.cardId)?.cardId,
79
- sessionId: contexts.find((context) => context.sessionId)?.sessionId,
80
- });
81
- }
82
- function stringOrNull(value) {
83
- if (typeof value !== "string")
84
- return null;
85
- return value.length > 0 ? value : null;
86
- }
@@ -1,8 +1,9 @@
1
- import { getAuth, requireAuthToken } from "./auth";
2
- import { isEdit, requireDeckId } from "./context";
1
+ import { isEdit } from "./context";
3
2
  import { CastleError } from "./errors";
4
- import { graphqlRequest } from "./graphql";
3
+ import { hostRequest } from "./transport";
5
4
  const LEADERBOARD_FLUSH_INTERVAL_MS = 5000;
5
+ // Host stamps deckId, so one deck session = one set of leaderboards; key by
6
+ // variable+scope only and keep the best high/low score per key until flush.
6
7
  const leaderboardWrites = new Map();
7
8
  let leaderboardFlushTimer = null;
8
9
  let leaderboardFlushPromise = null;
@@ -18,32 +19,78 @@ export const Leaderboard = {
18
19
  function writeLeaderboard(variable, score, options) {
19
20
  if (isEdit())
20
21
  return;
21
- void bufferLeaderboardWrite(variable, score, options).catch(reportLeaderboardError);
22
+ try {
23
+ bufferLeaderboardWrite(variable, score, options);
24
+ }
25
+ catch (error) {
26
+ reportLeaderboardError(error);
27
+ }
22
28
  }
23
29
  async function fetchLeaderboardData(variable, type, options) {
24
- const request = await leaderboardRequest("Leaderboard.fetch", variable, type, options);
25
- return normalizeLeaderboard(await fetchLeaderboard(request), request.userId);
30
+ assertLeaderboardVariable(variable, "Leaderboard.fetch");
31
+ assertLeaderboardType(type, "Leaderboard.fetch");
32
+ const scope = leaderboardScope(options);
33
+ // If the deck has written a score for this variable+scope this session, send
34
+ // it so the host writes-and-reads via leaderboardV2 and the player's own
35
+ // score shows up immediately (mirrors getLeaderboard in
36
+ // core/src/leaderboards.cpp — presence of a buffered score, not dirtiness,
37
+ // gates the write-through). Otherwise a plain read of the settled board.
38
+ const record = leaderboardWrites.get(leaderboardWriteKey(variable, scope));
39
+ const score = record
40
+ ? type === "high"
41
+ ? record.highScore
42
+ : record.lowScore
43
+ : null;
44
+ const { leaderboard, currentUserId } = await hostRequest("leaderboard.fetch", {
45
+ variable,
46
+ type,
47
+ scope,
48
+ ...(score === null ? {} : { score }),
49
+ });
50
+ if (record && score !== null) {
51
+ clearLeaderboardDirtyAfterFetch(record, type, score);
52
+ }
53
+ return normalizeLeaderboard(leaderboard, currentUserId);
54
+ }
55
+ // After a write-through fetch, clear the dirty flag for the side we just sent
56
+ // (so the periodic flush won't re-send it via saveVariableToLeaderboard). If
57
+ // the other side's buffered value matches what we sent (the common single-score
58
+ // case where high == low), clear it too. The equality guards skip clearing if a
59
+ // concurrent write bumped the buffered score while the fetch was in flight —
60
+ // that newer score still needs flushing. Mirrors leaderboards.cpp.
61
+ function clearLeaderboardDirtyAfterFetch(record, type, sentScore) {
62
+ if (type === "high") {
63
+ if (record.highScore === sentScore) {
64
+ record.isHighDirty = false;
65
+ if (record.lowScore === sentScore)
66
+ record.isLowDirty = false;
67
+ }
68
+ }
69
+ else {
70
+ if (record.lowScore === sentScore) {
71
+ record.isLowDirty = false;
72
+ if (record.highScore === sentScore)
73
+ record.isHighDirty = false;
74
+ }
75
+ }
26
76
  }
27
- async function bufferLeaderboardWrite(variable, score, options) {
77
+ function bufferLeaderboardWrite(variable, score, options) {
28
78
  assertLeaderboardVariable(variable, "Leaderboard.write");
29
79
  assertLeaderboardScore(score, "Leaderboard.write");
30
- const deckId = await requireDeckId("Leaderboard.write");
31
- await requireAuthToken("Leaderboard.write");
32
80
  const scope = leaderboardScope(options);
33
- const key = leaderboardWriteKey(deckId, variable, scope);
81
+ const key = leaderboardWriteKey(variable, scope);
34
82
  const record = leaderboardWrites.get(key);
35
83
  if (record) {
36
84
  updatePendingLeaderboardWrite(record, score);
37
85
  }
38
86
  else {
39
- leaderboardWrites.set(key, newPendingLeaderboardWrite(deckId, variable, scope, score));
87
+ leaderboardWrites.set(key, newPendingLeaderboardWrite(variable, scope, score));
40
88
  }
41
89
  ensureLeaderboardUnloadFlush();
42
90
  scheduleLeaderboardFlush();
43
91
  }
44
- function newPendingLeaderboardWrite(deckId, variable, scope, score) {
92
+ function newPendingLeaderboardWrite(variable, scope, score) {
45
93
  return {
46
- deckId,
47
94
  variable,
48
95
  scope,
49
96
  highScore: score,
@@ -89,12 +136,8 @@ async function flushLeaderboardWrites() {
89
136
  return leaderboardFlushPromise;
90
137
  }
91
138
  async function flushLeaderboardWritesOnce() {
92
- const jobs = leaderboardWriteJobs();
93
- if (jobs.length === 0)
94
- return;
95
- const token = await requireAuthToken("Leaderboard.write");
96
- for (const job of jobs) {
97
- await saveLeaderboardScore(job.record, job.score, token);
139
+ for (const job of leaderboardWriteJobs()) {
140
+ await saveLeaderboardScore(job.record, job.score);
98
141
  markLeaderboardWriteClean(job);
99
142
  }
100
143
  }
@@ -131,104 +174,13 @@ function hasDirtyLeaderboardWrites() {
131
174
  }
132
175
  return false;
133
176
  }
134
- async function leaderboardRequest(operation, variable, type, options) {
135
- assertLeaderboardVariable(variable, operation);
136
- assertLeaderboardType(type, operation);
137
- const [deckId, token, auth] = await Promise.all([
138
- requireDeckId(operation),
139
- requireAuthToken(operation),
140
- getAuth(),
141
- ]);
142
- return {
143
- deckId,
144
- variable,
145
- type,
146
- scope: leaderboardScope(options),
147
- token,
148
- userId: auth.userId ?? null,
149
- };
150
- }
151
- async function fetchLeaderboard(request) {
152
- const data = await graphqlRequest(`
153
- query CastleLeaderboard(
154
- $deckId: ID!
155
- $variable: String!
156
- $type: LeaderboardType!
157
- $filter: LeaderboardFilter!
158
- $includeFollowList: Boolean
159
- $includeParties: Boolean
160
- $scope: String
161
- ) {
162
- leaderboard(
163
- deckId: $deckId
164
- variable: $variable
165
- type: $type
166
- filter: $filter
167
- includeFollowList: $includeFollowList
168
- includeParties: $includeParties
169
- scope: $scope
170
- ) ${leaderboardFields()}
171
- }
172
- `, leaderboardVariables(request), {
173
- operation: "CastleLeaderboard",
174
- token: request.token,
175
- requireAuth: true,
176
- });
177
- return data.leaderboard;
178
- }
179
- async function saveLeaderboardScore(record, score, token) {
180
- await graphqlRequest(`
181
- mutation CastleSaveVariableToLeaderboard(
182
- $deckId: ID!
183
- $variable: String!
184
- $score: Float!
185
- $scope: String
186
- ) {
187
- saveVariableToLeaderboard(
188
- deckId: $deckId
189
- variable: $variable
190
- score: $score
191
- scope: $scope
192
- )
193
- }
194
- `, {
195
- deckId: record.deckId,
177
+ async function saveLeaderboardScore(record, score) {
178
+ await hostRequest("leaderboard.save", {
196
179
  variable: record.variable,
197
180
  score,
198
181
  scope: record.scope,
199
- }, {
200
- operation: "CastleSaveVariableToLeaderboard",
201
- token,
202
- requireAuth: true,
203
182
  });
204
183
  }
205
- function leaderboardVariables(request) {
206
- return {
207
- deckId: request.deckId,
208
- variable: request.variable,
209
- type: request.type,
210
- filter: "dedupUsers",
211
- includeFollowList: false,
212
- includeParties: false,
213
- scope: request.scope,
214
- };
215
- }
216
- function leaderboardFields() {
217
- return `{
218
- yourScore { score }
219
- list {
220
- place
221
- score
222
- user {
223
- userId
224
- username
225
- }
226
- }
227
- }`;
228
- }
229
- function leaderboardScope(options) {
230
- return options.scope ?? null;
231
- }
232
184
  function normalizeLeaderboard(leaderboard, userId) {
233
185
  const list = (leaderboard.list ?? []).map(normalizeLeaderboardEntry);
234
186
  const playerRank = playerRankFromList(list, userId);
@@ -261,8 +213,11 @@ function scoreNumber(value) {
261
213
  const parsed = Number.parseFloat(value);
262
214
  return Number.isFinite(parsed) ? parsed : undefined;
263
215
  }
264
- function leaderboardWriteKey(deckId, variable, scope) {
265
- return `${deckId}::${variable}${scope ? `::${scope}` : ""}`;
216
+ function leaderboardWriteKey(variable, scope) {
217
+ return `${variable}${scope ? `::${scope}` : ""}`;
218
+ }
219
+ function leaderboardScope(options) {
220
+ return options.scope ?? null;
266
221
  }
267
222
  function assertLeaderboardVariable(variable, operation) {
268
223
  if (variable.trim().length > 0)
@@ -0,0 +1,7 @@
1
+ import type { PassOfferResult } from "./commands";
2
+ export type { PassOfferResult, PassOfferStatus } from "./commands";
3
+ export interface CastlePassApi {
4
+ has(passId: string): Promise<boolean>;
5
+ offer(passId: string): Promise<PassOfferResult>;
6
+ }
7
+ export declare const Pass: CastlePassApi;
package/dist/passes.js ADDED
@@ -0,0 +1,84 @@
1
+ // Pass — a deck-facing capability for selling a creator "pass" to the player.
2
+ //
3
+ // The deck stays capability-AGNOSTIC: it just offers a pass to the player and
4
+ // gets back one normalized outcome regardless of platform. The host decides what
5
+ // UI to show and whether a real transaction can happen:
6
+ // - mobile app : renders the native bricks purchase sheet over the deck
7
+ // - web player : shows an "open in the app" upsell, returns `unavailable`
8
+ // - dev CLI : no host UI surface, so the SDK shows a minimal in-page
9
+ // notice itself (kept deliberately tiny), returns `unavailable`
10
+ // There is no capability check for the deck to make — every platform returns a
11
+ // PassOfferResult, so a single code path handles them all.
12
+ import { CastleError } from "./errors";
13
+ import { getCommandChannel, hostRequest } from "./transport";
14
+ export const Pass = {
15
+ has,
16
+ offer,
17
+ };
18
+ // Pure read — does the current player already own this pass? No UI, every
19
+ // platform answers it the same way (a GraphQL query). Use it to gate content or
20
+ // to decide whether to bother calling `offer`.
21
+ async function has(passId) {
22
+ if (typeof passId !== "string" || passId.length === 0) {
23
+ throw new CastleError({
24
+ code: "INVALID_ARGUMENT",
25
+ message: "Pass.has requires a passId.",
26
+ operation: "Pass.has",
27
+ });
28
+ }
29
+ const { hasPass } = await hostRequest("pass.has", { passId });
30
+ return hasPass;
31
+ }
32
+ async function offer(passId) {
33
+ if (typeof passId !== "string" || passId.length === 0) {
34
+ throw new CastleError({
35
+ code: "INVALID_ARGUMENT",
36
+ message: "Pass.offer requires a passId.",
37
+ operation: "Pass.offer",
38
+ });
39
+ }
40
+ const result = await hostRequest("pass.offer", { passId });
41
+ // On the dev server there's no host chrome to explain why nothing happened,
42
+ // so surface a small built-in notice. The mobile/web hosts render their own
43
+ // UI, so the SDK stays silent there.
44
+ if (result.status === "unavailable" && getCommandChannel() === "local") {
45
+ showDevUnavailableNotice();
46
+ }
47
+ return result;
48
+ }
49
+ let devNoticeEl = null;
50
+ let devNoticeTimer = null;
51
+ // Minimal, dependency-free toast. Dev-only affordance — not the place for a
52
+ // designed purchase UI.
53
+ function showDevUnavailableNotice() {
54
+ if (typeof document === "undefined")
55
+ return;
56
+ if (!devNoticeEl) {
57
+ devNoticeEl = document.createElement("div");
58
+ devNoticeEl.textContent = "Passes can only be purchased in the Castle app.";
59
+ devNoticeEl.style.cssText = [
60
+ "position:fixed",
61
+ "left:50%",
62
+ "bottom:24px",
63
+ "transform:translateX(-50%)",
64
+ "max-width:80vw",
65
+ "padding:10px 16px",
66
+ "border-radius:8px",
67
+ "background:rgba(0,0,0,0.82)",
68
+ "color:#fff",
69
+ "font:500 13px/1.4 system-ui,sans-serif",
70
+ "text-align:center",
71
+ "z-index:2147483647",
72
+ "pointer-events:none",
73
+ "transition:opacity 0.3s ease",
74
+ ].join(";");
75
+ document.body.appendChild(devNoticeEl);
76
+ }
77
+ devNoticeEl.style.opacity = "1";
78
+ if (devNoticeTimer)
79
+ clearTimeout(devNoticeTimer);
80
+ devNoticeTimer = setTimeout(() => {
81
+ if (devNoticeEl)
82
+ devNoticeEl.style.opacity = "0";
83
+ }, 3200);
84
+ }
package/dist/runtime.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type CommandName, type CommandParams, type CommandResponseEnvelope } from "./commands";
1
2
  export declare const CARD_RATIO: number;
2
3
  interface LocalResponse {
3
4
  type: string;
@@ -9,4 +10,6 @@ interface LocalResponse {
9
10
  export declare function setup(): void;
10
11
  export declare function writeFile(path: string, contents: string): Promise<LocalResponse>;
11
12
  export declare function initCard(): HTMLDivElement;
13
+ export declare function sendLocalCommand<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResponseEnvelope>;
14
+ export declare function onBeforeRestart(hook: () => void | Promise<void>): () => void;
12
15
  export {};