@porulle/plugin-channel-connector 0.66.0 → 0.67.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/dist/index.d.ts +1 -1
- package/dist/index.js +7 -2
- package/dist/oauth-state.d.ts +2 -0
- package/dist/oauth-state.js +4 -0
- package/dist/service.d.ts +16 -0
- package/dist/service.js +12 -1
- package/package.json +2 -2
- package/src/index.ts +7 -2
- package/src/oauth-state.ts +6 -0
- package/src/service.ts +26 -2
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type { CatalogFieldMapping, CatalogFieldMappingInput, CatalogFieldMapping
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const CHANNEL_MAX_BATCHES_PER_SWEEP = 5000;
|
|
27
27
|
export { signState, verifyState } from "./oauth-state.js";
|
|
28
|
-
export type { BackfillCatalogOptions, BackfillCatalogReport, BuildCatalogPushItemsOptions, BuildCatalogPushItemsResult, CatalogPushAssemblyField, CatalogPushAssemblyImage, CatalogPushAssemblyItem, CatalogPushPreviewBefore, CatalogPushPreviewBeforeStatus, CatalogPushPreviewDiff, CatalogPushPreviewItem, CatalogPushPreviewResult, CatalogPushPreviewUnavailable, PushCatalogToStoreResult, CatalogPushJobResult, CatalogConvergenceFailure, CatalogFieldConflict, CatalogFieldSkip, CatalogPushFieldSkip, CatalogPushSkipReason, CatalogConflictState, CatalogWriteSettings, AfterStoreConnected, BindConnectedStore, ChannelComplianceData, ChannelConnectorPluginOptions, ConfineStores, OnStoreCatalogChanged, StoreConnectActor, StoreReadContext, ChannelStockLine, ExportState, PublicConnectedStore, ReconcileReport, } from "./service.js";
|
|
28
|
+
export type { BackfillCatalogOptions, BackfillCatalogReport, BuildCatalogPushItemsOptions, BuildCatalogPushItemsResult, CatalogPushAssemblyField, CatalogPushAssemblyImage, CatalogPushAssemblyItem, CatalogPushPreviewBefore, CatalogPushPreviewBeforeStatus, CatalogPushPreviewDiff, CatalogPushPreviewItem, CatalogPushPreviewResult, CatalogPushPreviewUnavailable, PushCatalogToStoreResult, CatalogPushJobResult, CatalogConvergenceFailure, CatalogFieldConflict, CatalogFieldSkip, CatalogPushFieldSkip, CatalogPushSkipReason, CatalogConflictState, CatalogWriteSettings, AfterStoreConnected, BindConnectedStore, ChannelComplianceData, ChannelConnectorPluginOptions, ConfineStores, ConnectClaims, OnStoreCatalogChanged, StoreConnectActor, StoreReadContext, ChannelStockLine, ExportState, PublicConnectedStore, ReconcileReport, } from "./service.js";
|
|
29
29
|
export type { OAuthStatePayload, OAuthStateResult } from "./oauth-state.js";
|
|
30
30
|
export type { ChannelCatalogPush, ChannelCatalogPushEvent, ChannelCatalogConflict, ChannelCatalogConflictEvent, ChannelEntityMapEntry, ChannelExportEvent, ChannelOrderExport, ChannelRefundEvent, ChannelRefundRequest, ConnectedStore, } from "./schema.js";
|
|
31
31
|
export declare function channelConnectorPlugin(options?: ChannelConnectorPluginOptions): import("@porulle/core").CommercePlugin;
|
package/dist/index.js
CHANGED
|
@@ -361,6 +361,9 @@ export function channelConnectorPlugin(options = {}) {
|
|
|
361
361
|
const userId = actor?.userId;
|
|
362
362
|
if (!userId)
|
|
363
363
|
return oauthError(403, "USER_REQUIRED", "Connecting a store needs a signed-in user.");
|
|
364
|
+
const claims = await service.connectClaims({ orgId, actor, raw });
|
|
365
|
+
if (!claims.ok)
|
|
366
|
+
return connectOutcome(oauth.postConnectRedirect, { error: claims.code ?? "CONNECT_REFUSED", message: claims.error });
|
|
364
367
|
const typed = String(query.shop ?? query.store ?? "");
|
|
365
368
|
const storeDomain = connector.normalizeStoreDomain ? connector.normalizeStoreDomain(typed) : typed;
|
|
366
369
|
if (!storeDomain)
|
|
@@ -369,6 +372,7 @@ export function channelConnectorPlugin(options = {}) {
|
|
|
369
372
|
provider,
|
|
370
373
|
orgId,
|
|
371
374
|
userId,
|
|
375
|
+
claims: claims.value,
|
|
372
376
|
shopDomain: storeDomain,
|
|
373
377
|
exp: Math.floor(Date.now() / 1000) + 600,
|
|
374
378
|
jti: crypto.randomUUID(),
|
|
@@ -423,7 +427,7 @@ export function channelConnectorPlugin(options = {}) {
|
|
|
423
427
|
provider,
|
|
424
428
|
storeDomain: verified.value.shopDomain,
|
|
425
429
|
credentials: completed.value.credentials,
|
|
426
|
-
}, { orgId: verified.value.orgId, userId: verified.value.userId });
|
|
430
|
+
}, { orgId: verified.value.orgId, userId: verified.value.userId, claims: verified.value.claims });
|
|
427
431
|
if (!connected.ok)
|
|
428
432
|
return refused(connected.code ?? "STORE_CONNECTION_FAILED", connected.error);
|
|
429
433
|
return connectOutcome(oauth.postConnectRedirect, { connected: connected.value.id });
|
|
@@ -510,7 +514,8 @@ export function channelConnectorPlugin(options = {}) {
|
|
|
510
514
|
webhookSecret: z.string().min(1).optional(),
|
|
511
515
|
}))
|
|
512
516
|
.handler(async ({ input, orgId, actor, raw }) => {
|
|
513
|
-
|
|
517
|
+
const claims = unwrap(await service.connectClaims({ orgId, actor, raw }));
|
|
518
|
+
return unwrap(await service.connectStore(orgId, input, { orgId, userId: actor?.userId ?? null, claims, raw }));
|
|
514
519
|
});
|
|
515
520
|
channels.get("/stores")
|
|
516
521
|
.summary("List connected channel stores")
|
package/dist/oauth-state.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface OAuthStatePayload {
|
|
|
3
3
|
orgId: string;
|
|
4
4
|
/** The signed-in user who started the connection: the callback arrives with no session of its own. */
|
|
5
5
|
userId: string;
|
|
6
|
+
/** The consumer's connect claims, resolved at start. */
|
|
7
|
+
claims: Record<string, string>;
|
|
6
8
|
shopDomain: string;
|
|
7
9
|
exp: number;
|
|
8
10
|
jti: string;
|
package/dist/oauth-state.js
CHANGED
|
@@ -68,6 +68,10 @@ export function verifyState(state, secret, now = Math.floor(Date.now() / 1000))
|
|
|
68
68
|
return { ok: false, error: "Malformed OAuth state payload." };
|
|
69
69
|
if (exp <= now)
|
|
70
70
|
return { ok: false, error: "OAuth state has expired." };
|
|
71
|
+
const claims = candidate.claims;
|
|
72
|
+
if (typeof claims !== "object" || claims === null || Object.values(claims).some((value) => typeof value !== "string")) {
|
|
73
|
+
return { ok: false, error: "Malformed OAuth state payload." };
|
|
74
|
+
}
|
|
71
75
|
return { ok: true, value: candidate };
|
|
72
76
|
}
|
|
73
77
|
export function oauthStateEventId(jti) {
|
package/dist/service.d.ts
CHANGED
|
@@ -109,9 +109,20 @@ export type ConfineStores = (context: StoreReadContext) => Promise<readonly stri
|
|
|
109
109
|
export interface StoreConnectActor {
|
|
110
110
|
orgId: string;
|
|
111
111
|
userId: string | null;
|
|
112
|
+
/**
|
|
113
|
+
* What the consumer's {@link ConnectClaims} resolved when the connection started — e.g. which of
|
|
114
|
+
* the user's vendors the store is for. Carried in the signed OAuth state, so the callback, which has
|
|
115
|
+
* no session or headers of its own, binds the store to exactly what was chosen at the start.
|
|
116
|
+
*/
|
|
117
|
+
claims: Readonly<Record<string, string>>;
|
|
112
118
|
/** Core's request escape hatch when the connect is a request; absent on the OAuth callback. */
|
|
113
119
|
raw?: unknown;
|
|
114
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolves, from the request that STARTS a connection, the facts the consumer needs to bind the store
|
|
123
|
+
* later. Throw to refuse the start. Runs for OAuth start and for `POST /stores`.
|
|
124
|
+
*/
|
|
125
|
+
export type ConnectClaims = (context: StoreReadContext) => Promise<Record<string, string>> | Record<string, string>;
|
|
115
126
|
/**
|
|
116
127
|
* Binds a just-connected store to whatever the consumer means by an owner, INSIDE the transaction
|
|
117
128
|
* that wrote the store row — so a store and its binding commit together or not at all. Throw to
|
|
@@ -127,6 +138,7 @@ export type AfterStoreConnected = (input: {
|
|
|
127
138
|
store: ConnectedStore;
|
|
128
139
|
actor: StoreConnectActor;
|
|
129
140
|
connector: ChannelConnector;
|
|
141
|
+
services: Record<string, unknown>;
|
|
130
142
|
}) => Promise<void>;
|
|
131
143
|
/** Entities a provider webhook just created or changed, converged; the host projects them. */
|
|
132
144
|
export type OnStoreCatalogChanged = (input: {
|
|
@@ -154,6 +166,8 @@ export interface ChannelConnectorPluginOptions {
|
|
|
154
166
|
* unaffected.
|
|
155
167
|
*/
|
|
156
168
|
confineStores?: ConfineStores;
|
|
169
|
+
/** See {@link ConnectClaims}. Absent, a connection carries no claims. */
|
|
170
|
+
connectClaims?: ConnectClaims;
|
|
157
171
|
/** See {@link BindConnectedStore}. */
|
|
158
172
|
bindConnectedStore?: BindConnectedStore;
|
|
159
173
|
/** See {@link AfterStoreConnected}. */
|
|
@@ -495,6 +509,8 @@ export declare class ChannelConnectorService {
|
|
|
495
509
|
}, actor: StoreConnectActor): Promise<PluginResult<PublicConnectedStore>>;
|
|
496
510
|
/** The store with credentials good for a call the host makes itself, e.g. its own Admin API write. */
|
|
497
511
|
liveStore(orgId: string, storeId: string): Promise<PluginResult<ChannelStore>>;
|
|
512
|
+
/** The consumer's claims for a connection starting from this request. See {@link ConnectClaims}. */
|
|
513
|
+
connectClaims(context: StoreReadContext): Promise<PluginResult<Record<string, string>>>;
|
|
498
514
|
/** The caller's allow-list, or null for unconfined. See {@link ConfineStores}. */
|
|
499
515
|
private allowedStores;
|
|
500
516
|
/** NOT_FOUND for a store outside the caller's set, exactly as for one that does not exist. */
|
package/dist/service.js
CHANGED
|
@@ -2269,7 +2269,7 @@ export class ChannelConnectorService {
|
|
|
2269
2269
|
}
|
|
2270
2270
|
if (this.options.afterStoreConnected) {
|
|
2271
2271
|
try {
|
|
2272
|
-
await this.options.afterStoreConnected({ store, actor, connector });
|
|
2272
|
+
await this.options.afterStoreConnected({ store, actor, connector, services: this.services });
|
|
2273
2273
|
}
|
|
2274
2274
|
catch (error) {
|
|
2275
2275
|
return PluginErr(error instanceof Error ? error.message : "The store connected but its follow-on work failed.", "AFTER_CONNECT_FAILED");
|
|
@@ -2288,6 +2288,17 @@ export class ChannelConnectorService {
|
|
|
2288
2288
|
const live = await resolveLiveCredentials(connector, this.db, store);
|
|
2289
2289
|
return live.ok ? Ok(live.value) : PluginErr(live.error.message, live.error.code);
|
|
2290
2290
|
}
|
|
2291
|
+
/** The consumer's claims for a connection starting from this request. See {@link ConnectClaims}. */
|
|
2292
|
+
async connectClaims(context) {
|
|
2293
|
+
if (!this.options.connectClaims)
|
|
2294
|
+
return Ok({});
|
|
2295
|
+
try {
|
|
2296
|
+
return Ok(await this.options.connectClaims(context));
|
|
2297
|
+
}
|
|
2298
|
+
catch (error) {
|
|
2299
|
+
return PluginErr(error instanceof Error ? error.message : "The connection could not be started.", error instanceof CommerceNotFoundError ? "NOT_FOUND" : "CONNECT_REFUSED");
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2291
2302
|
/** The caller's allow-list, or null for unconfined. See {@link ConfineStores}. */
|
|
2292
2303
|
async allowedStores(orgId, context) {
|
|
2293
2304
|
return this.options.confineStores ? await this.options.confineStores(context ?? { orgId, actor: null, raw: undefined }) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-channel-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@hono/zod-openapi": "^1.2.2",
|
|
24
24
|
"hono": "^4.12.5",
|
|
25
|
-
"@porulle/core": "0.
|
|
25
|
+
"@porulle/core": "0.67.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^24.5.2",
|
package/src/index.ts
CHANGED
|
@@ -238,6 +238,7 @@ export type {
|
|
|
238
238
|
ChannelComplianceData,
|
|
239
239
|
ChannelConnectorPluginOptions,
|
|
240
240
|
ConfineStores,
|
|
241
|
+
ConnectClaims,
|
|
241
242
|
OnStoreCatalogChanged,
|
|
242
243
|
StoreConnectActor,
|
|
243
244
|
StoreReadContext,
|
|
@@ -531,6 +532,8 @@ export function channelConnectorPlugin(options: ChannelConnectorPluginOptions =
|
|
|
531
532
|
// here, carried in the signed state. A caller with no user (an API key) cannot start one.
|
|
532
533
|
const userId = actor?.userId;
|
|
533
534
|
if (!userId) return oauthError(403, "USER_REQUIRED", "Connecting a store needs a signed-in user.");
|
|
535
|
+
const claims = await service.connectClaims({ orgId, actor, raw });
|
|
536
|
+
if (!claims.ok) return connectOutcome(oauth.postConnectRedirect, { error: claims.code ?? "CONNECT_REFUSED", message: claims.error });
|
|
534
537
|
const typed = String((query as { shop?: string; store?: string }).shop ?? (query as { store?: string }).store ?? "");
|
|
535
538
|
const storeDomain = connector.normalizeStoreDomain ? connector.normalizeStoreDomain(typed) : typed;
|
|
536
539
|
if (!storeDomain) return connectOutcome(oauth.postConnectRedirect, { error: "INVALID_STORE_DOMAIN", message: `"${typed}" does not name a ${provider} store.` });
|
|
@@ -538,6 +541,7 @@ export function channelConnectorPlugin(options: ChannelConnectorPluginOptions =
|
|
|
538
541
|
provider,
|
|
539
542
|
orgId,
|
|
540
543
|
userId,
|
|
544
|
+
claims: claims.value,
|
|
541
545
|
shopDomain: storeDomain,
|
|
542
546
|
exp: Math.floor(Date.now() / 1000) + 600,
|
|
543
547
|
jti: crypto.randomUUID(),
|
|
@@ -583,7 +587,7 @@ export function channelConnectorPlugin(options: ChannelConnectorPluginOptions =
|
|
|
583
587
|
provider,
|
|
584
588
|
storeDomain: verified.value.shopDomain,
|
|
585
589
|
credentials: completed.value.credentials,
|
|
586
|
-
}, { orgId: verified.value.orgId, userId: verified.value.userId });
|
|
590
|
+
}, { orgId: verified.value.orgId, userId: verified.value.userId, claims: verified.value.claims });
|
|
587
591
|
if (!connected.ok) return refused(connected.code ?? "STORE_CONNECTION_FAILED", connected.error);
|
|
588
592
|
return connectOutcome(oauth.postConnectRedirect, { connected: connected.value.id });
|
|
589
593
|
};
|
|
@@ -665,6 +669,7 @@ export function channelConnectorPlugin(options: ChannelConnectorPluginOptions =
|
|
|
665
669
|
webhookSecret: z.string().min(1).optional(),
|
|
666
670
|
}))
|
|
667
671
|
.handler(async ({ input, orgId, actor, raw }: ChannelRouteContext) => {
|
|
672
|
+
const claims = unwrap(await service.connectClaims({ orgId, actor, raw }));
|
|
668
673
|
return unwrap(await service.connectStore(
|
|
669
674
|
orgId,
|
|
670
675
|
input as {
|
|
@@ -673,7 +678,7 @@ export function channelConnectorPlugin(options: ChannelConnectorPluginOptions =
|
|
|
673
678
|
storeDomain: string;
|
|
674
679
|
webhookSecret?: string;
|
|
675
680
|
},
|
|
676
|
-
{ orgId, userId: actor?.userId ?? null, raw },
|
|
681
|
+
{ orgId, userId: actor?.userId ?? null, claims, raw },
|
|
677
682
|
));
|
|
678
683
|
});
|
|
679
684
|
|
package/src/oauth-state.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface OAuthStatePayload {
|
|
|
5
5
|
orgId: string;
|
|
6
6
|
/** The signed-in user who started the connection: the callback arrives with no session of its own. */
|
|
7
7
|
userId: string;
|
|
8
|
+
/** The consumer's connect claims, resolved at start. */
|
|
9
|
+
claims: Record<string, string>;
|
|
8
10
|
shopDomain: string;
|
|
9
11
|
exp: number;
|
|
10
12
|
jti: string;
|
|
@@ -86,6 +88,10 @@ export function verifyState(
|
|
|
86
88
|
!Number.isInteger(exp)
|
|
87
89
|
) return { ok: false, error: "Malformed OAuth state payload." };
|
|
88
90
|
if (exp <= now) return { ok: false, error: "OAuth state has expired." };
|
|
91
|
+
const claims: unknown = (candidate as { claims?: unknown }).claims;
|
|
92
|
+
if (typeof claims !== "object" || claims === null || Object.values(claims).some((value) => typeof value !== "string")) {
|
|
93
|
+
return { ok: false, error: "Malformed OAuth state payload." };
|
|
94
|
+
}
|
|
89
95
|
|
|
90
96
|
return { ok: true, value: candidate as OAuthStatePayload };
|
|
91
97
|
}
|
package/src/service.ts
CHANGED
|
@@ -254,10 +254,22 @@ export type ConfineStores = (context: StoreReadContext) => Promise<readonly stri
|
|
|
254
254
|
export interface StoreConnectActor {
|
|
255
255
|
orgId: string;
|
|
256
256
|
userId: string | null;
|
|
257
|
+
/**
|
|
258
|
+
* What the consumer's {@link ConnectClaims} resolved when the connection started — e.g. which of
|
|
259
|
+
* the user's vendors the store is for. Carried in the signed OAuth state, so the callback, which has
|
|
260
|
+
* no session or headers of its own, binds the store to exactly what was chosen at the start.
|
|
261
|
+
*/
|
|
262
|
+
claims: Readonly<Record<string, string>>;
|
|
257
263
|
/** Core's request escape hatch when the connect is a request; absent on the OAuth callback. */
|
|
258
264
|
raw?: unknown;
|
|
259
265
|
}
|
|
260
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Resolves, from the request that STARTS a connection, the facts the consumer needs to bind the store
|
|
269
|
+
* later. Throw to refuse the start. Runs for OAuth start and for `POST /stores`.
|
|
270
|
+
*/
|
|
271
|
+
export type ConnectClaims = (context: StoreReadContext) => Promise<Record<string, string>> | Record<string, string>;
|
|
272
|
+
|
|
261
273
|
/**
|
|
262
274
|
* Binds a just-connected store to whatever the consumer means by an owner, INSIDE the transaction
|
|
263
275
|
* that wrote the store row — so a store and its binding commit together or not at all. Throw to
|
|
@@ -266,7 +278,7 @@ export interface StoreConnectActor {
|
|
|
266
278
|
export type BindConnectedStore = (input: { db: PluginDb; store: ConnectedStore; actor: StoreConnectActor }) => Promise<void>;
|
|
267
279
|
|
|
268
280
|
/** Work that follows a committed connection: the first import, provider-attested facts, keys. */
|
|
269
|
-
export type AfterStoreConnected = (input: { store: ConnectedStore; actor: StoreConnectActor; connector: ChannelConnector }) => Promise<void>;
|
|
281
|
+
export type AfterStoreConnected = (input: { store: ConnectedStore; actor: StoreConnectActor; connector: ChannelConnector; services: Record<string, unknown> }) => Promise<void>;
|
|
270
282
|
|
|
271
283
|
/** Entities a provider webhook just created or changed, converged; the host projects them. */
|
|
272
284
|
export type OnStoreCatalogChanged = (input: { orgId: string; storeId: string; entityIds: string[]; convergence: CatalogPageConvergence }) => Promise<void>;
|
|
@@ -288,6 +300,8 @@ export interface ChannelConnectorPluginOptions {
|
|
|
288
300
|
* unaffected.
|
|
289
301
|
*/
|
|
290
302
|
confineStores?: ConfineStores;
|
|
303
|
+
/** See {@link ConnectClaims}. Absent, a connection carries no claims. */
|
|
304
|
+
connectClaims?: ConnectClaims;
|
|
291
305
|
/** See {@link BindConnectedStore}. */
|
|
292
306
|
bindConnectedStore?: BindConnectedStore;
|
|
293
307
|
/** See {@link AfterStoreConnected}. */
|
|
@@ -3165,7 +3179,7 @@ export class ChannelConnectorService {
|
|
|
3165
3179
|
}
|
|
3166
3180
|
if (this.options.afterStoreConnected) {
|
|
3167
3181
|
try {
|
|
3168
|
-
await this.options.afterStoreConnected({ store, actor, connector });
|
|
3182
|
+
await this.options.afterStoreConnected({ store, actor, connector, services: this.services });
|
|
3169
3183
|
} catch (error) {
|
|
3170
3184
|
return PluginErr(error instanceof Error ? error.message : "The store connected but its follow-on work failed.", "AFTER_CONNECT_FAILED");
|
|
3171
3185
|
}
|
|
@@ -3183,6 +3197,16 @@ export class ChannelConnectorService {
|
|
|
3183
3197
|
return live.ok ? Ok(live.value) : PluginErr(live.error.message, live.error.code);
|
|
3184
3198
|
}
|
|
3185
3199
|
|
|
3200
|
+
/** The consumer's claims for a connection starting from this request. See {@link ConnectClaims}. */
|
|
3201
|
+
async connectClaims(context: StoreReadContext): Promise<PluginResult<Record<string, string>>> {
|
|
3202
|
+
if (!this.options.connectClaims) return Ok({});
|
|
3203
|
+
try {
|
|
3204
|
+
return Ok(await this.options.connectClaims(context));
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
return PluginErr(error instanceof Error ? error.message : "The connection could not be started.", error instanceof CommerceNotFoundError ? "NOT_FOUND" : "CONNECT_REFUSED");
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3186
3210
|
/** The caller's allow-list, or null for unconfined. See {@link ConfineStores}. */
|
|
3187
3211
|
private async allowedStores(orgId: string, context: StoreReadContext | undefined): Promise<readonly string[] | null> {
|
|
3188
3212
|
return this.options.confineStores ? await this.options.confineStores(context ?? { orgId, actor: null, raw: undefined }) : null;
|