@porulle/plugin-channel-connector 0.42.0 → 0.43.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.js +1 -1
- package/dist/service.d.ts +31 -1
- package/dist/service.js +19 -5
- package/package.json +2 -2
- package/src/index.ts +9 -1
- package/src/service.ts +47 -5
package/dist/index.js
CHANGED
|
@@ -529,7 +529,7 @@ export function channelConnectorPlugin(options = {}) {
|
|
|
529
529
|
channels.get("/stores")
|
|
530
530
|
.summary("List connected channel stores")
|
|
531
531
|
.permission("channels:read")
|
|
532
|
-
.handler(async ({ orgId }) => unwrap(await service.listStores(orgId)));
|
|
532
|
+
.handler(async ({ orgId, actor, raw }) => unwrap(await service.listStores(orgId, { orgId, actor, raw })));
|
|
533
533
|
channels.get("/stores/{id}")
|
|
534
534
|
.summary("Get a connected channel store")
|
|
535
535
|
.permission("channels:read")
|
package/dist/service.d.ts
CHANGED
|
@@ -84,8 +84,38 @@ export interface ChannelComplianceData {
|
|
|
84
84
|
customerData: NonNullable<ChannelOrderExport["customerData"]>;
|
|
85
85
|
}>;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* What a consumer is allowed to read, resolved per request.
|
|
89
|
+
*
|
|
90
|
+
* Returning `null` means "do not confine" and is the default — every existing consumer keeps the
|
|
91
|
+
* organization-wide behaviour. An array is the complete set of store ids this caller may see, and
|
|
92
|
+
* **`[]` means none**, not "no filter".
|
|
93
|
+
*
|
|
94
|
+
* It takes IDS rather than a tenant, deliberately. `vendor`, `seller`, `team` are models a consumer
|
|
95
|
+
* owns; this package is generic commerce and acquiring one of them here would push a marketplace
|
|
96
|
+
* concept into every deployment that has no such thing. The consumer resolves the meaning and hands
|
|
97
|
+
* back the answer.
|
|
98
|
+
*/
|
|
99
|
+
export type ConfineStoreReads = (context: StoreReadContext) => Promise<readonly string[] | null> | readonly string[] | null;
|
|
100
|
+
/** What a consumer needs to resolve the caller. `raw` is core's documented request escape hatch. */
|
|
101
|
+
export interface StoreReadContext {
|
|
102
|
+
orgId: string;
|
|
103
|
+
actor: {
|
|
104
|
+
userId: string | null;
|
|
105
|
+
[key: string]: unknown;
|
|
106
|
+
} | null;
|
|
107
|
+
raw: unknown;
|
|
108
|
+
}
|
|
87
109
|
export interface ChannelConnectorPluginOptions {
|
|
88
110
|
connectors?: ChannelConnector[];
|
|
111
|
+
/**
|
|
112
|
+
* Confines store reads to a set the consumer chooses. See {@link ConfineStoreReads}.
|
|
113
|
+
*
|
|
114
|
+
* Absent by default, because narrowing an existing read for every deployment would be a breaking
|
|
115
|
+
* change to a published package. A consumer that needs confinement opts in; one that does not is
|
|
116
|
+
* unaffected.
|
|
117
|
+
*/
|
|
118
|
+
confineStoreReads?: ConfineStoreReads;
|
|
89
119
|
oauth?: {
|
|
90
120
|
stateSecret: string;
|
|
91
121
|
postConnectRedirect: string;
|
|
@@ -241,7 +271,7 @@ export declare class ChannelConnectorService {
|
|
|
241
271
|
disconnectStore(orgId: string, id: string): Promise<PluginResult<PublicConnectedStore>>;
|
|
242
272
|
disconnectStoreSystem(orgId: string, id: string, redactDomain?: boolean): Promise<PluginResult<PublicConnectedStore>>;
|
|
243
273
|
getStore(orgId: string, id: string): Promise<PluginResult<PublicConnectedStore>>;
|
|
244
|
-
listStores(orgId: string): Promise<PluginResult<PublicConnectedStore[]>>;
|
|
274
|
+
listStores(orgId: string, context?: StoreReadContext): Promise<PluginResult<PublicConnectedStore[]>>;
|
|
245
275
|
validateLineStock(orgId: string, lines: ChannelStockLine[], timeoutMs?: number): Promise<void>;
|
|
246
276
|
importCatalog(orgId: string, storeId: string, actor: Actor, options: {
|
|
247
277
|
maxItems: number;
|
package/dist/service.js
CHANGED
|
@@ -1612,11 +1612,25 @@ export class ChannelConnectorService {
|
|
|
1612
1612
|
return PluginErr("Connected store not found.", "NOT_FOUND");
|
|
1613
1613
|
return Ok(redactStore(store));
|
|
1614
1614
|
}
|
|
1615
|
-
async listStores(orgId) {
|
|
1616
|
-
const
|
|
1617
|
-
.
|
|
1618
|
-
|
|
1619
|
-
|
|
1615
|
+
async listStores(orgId, context) {
|
|
1616
|
+
const allowed = this.options.confineStoreReads
|
|
1617
|
+
? await this.options.confineStoreReads(context ?? { orgId, actor: null, raw: undefined })
|
|
1618
|
+
: null;
|
|
1619
|
+
// An empty allow-list means the caller may read NOTHING, stated here rather than left to the
|
|
1620
|
+
// query builder.
|
|
1621
|
+
//
|
|
1622
|
+
// MEASURED, because the first version of this comment claimed the guard was load-bearing and it
|
|
1623
|
+
// is not: removing this line leaves every row in `confine-store-reads.test.ts` green, so drizzle
|
|
1624
|
+
// already turns `inArray(id, [])` into a predicate that matches nothing. The guard is therefore
|
|
1625
|
+
// the CONTRACT rather than the rescue — `[]` means none, whatever the builder does with an empty
|
|
1626
|
+
// array on some future dialect or version. Worth keeping for that reason and not worth claiming
|
|
1627
|
+
// more for: the row that covers this case is really watching drizzle, not this line.
|
|
1628
|
+
if (allowed !== null && allowed.length === 0)
|
|
1629
|
+
return Ok([]);
|
|
1630
|
+
const predicate = allowed === null
|
|
1631
|
+
? eq(connectedStores.organizationId, orgId)
|
|
1632
|
+
: and(eq(connectedStores.organizationId, orgId), inArray(connectedStores.id, [...allowed]));
|
|
1633
|
+
const rows = await this.db.select().from(connectedStores).where(predicate);
|
|
1620
1634
|
return Ok(rows.map(redactStore));
|
|
1621
1635
|
}
|
|
1622
1636
|
async validateLineStock(orgId, lines, timeoutMs = 3_000) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-channel-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hono/zod-openapi": "^1.2.2",
|
|
21
21
|
"hono": "^4.12.5",
|
|
22
|
-
"@porulle/core": "0.
|
|
22
|
+
"@porulle/core": "0.43.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^24.5.2",
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,13 @@ type ChannelRouteContext = {
|
|
|
44
44
|
params: Record<string, string>;
|
|
45
45
|
orgId: string;
|
|
46
46
|
actor: { userId: string | null } | null;
|
|
47
|
+
/**
|
|
48
|
+
* Core's documented request escape hatch, already present on `RouteHandlerContext` and simply
|
|
49
|
+
* never declared here. Read by `confineStoreReads` so a consumer can resolve the caller from the
|
|
50
|
+
* request — core's `actor.vendorId` cannot serve that purpose: core sources it from a column on
|
|
51
|
+
* `user` that this deployment never writes, and sets it to `null` outright for API-key actors.
|
|
52
|
+
*/
|
|
53
|
+
raw?: unknown;
|
|
47
54
|
};
|
|
48
55
|
|
|
49
56
|
export { mockChannelConnector } from "./mock-connector.js";
|
|
@@ -655,7 +662,8 @@ export function channelConnectorPlugin(options: ChannelConnectorPluginOptions =
|
|
|
655
662
|
channels.get("/stores")
|
|
656
663
|
.summary("List connected channel stores")
|
|
657
664
|
.permission("channels:read")
|
|
658
|
-
.handler(async ({ orgId }: ChannelRouteContext) =>
|
|
665
|
+
.handler(async ({ orgId, actor, raw }: ChannelRouteContext) =>
|
|
666
|
+
unwrap(await service.listStores(orgId, { orgId, actor, raw })));
|
|
659
667
|
|
|
660
668
|
channels.get("/stores/{id}")
|
|
661
669
|
.summary("Get a connected channel store")
|
package/src/service.ts
CHANGED
|
@@ -214,8 +214,37 @@ export interface ChannelComplianceData {
|
|
|
214
214
|
}>;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* What a consumer is allowed to read, resolved per request.
|
|
219
|
+
*
|
|
220
|
+
* Returning `null` means "do not confine" and is the default — every existing consumer keeps the
|
|
221
|
+
* organization-wide behaviour. An array is the complete set of store ids this caller may see, and
|
|
222
|
+
* **`[]` means none**, not "no filter".
|
|
223
|
+
*
|
|
224
|
+
* It takes IDS rather than a tenant, deliberately. `vendor`, `seller`, `team` are models a consumer
|
|
225
|
+
* owns; this package is generic commerce and acquiring one of them here would push a marketplace
|
|
226
|
+
* concept into every deployment that has no such thing. The consumer resolves the meaning and hands
|
|
227
|
+
* back the answer.
|
|
228
|
+
*/
|
|
229
|
+
export type ConfineStoreReads = (context: StoreReadContext) => Promise<readonly string[] | null> | readonly string[] | null;
|
|
230
|
+
|
|
231
|
+
/** What a consumer needs to resolve the caller. `raw` is core's documented request escape hatch. */
|
|
232
|
+
export interface StoreReadContext {
|
|
233
|
+
orgId: string;
|
|
234
|
+
actor: { userId: string | null; [key: string]: unknown } | null;
|
|
235
|
+
raw: unknown;
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
export interface ChannelConnectorPluginOptions {
|
|
218
239
|
connectors?: ChannelConnector[];
|
|
240
|
+
/**
|
|
241
|
+
* Confines store reads to a set the consumer chooses. See {@link ConfineStoreReads}.
|
|
242
|
+
*
|
|
243
|
+
* Absent by default, because narrowing an existing read for every deployment would be a breaking
|
|
244
|
+
* change to a published package. A consumer that needs confinement opts in; one that does not is
|
|
245
|
+
* unaffected.
|
|
246
|
+
*/
|
|
247
|
+
confineStoreReads?: ConfineStoreReads;
|
|
219
248
|
oauth?: { stateSecret: string; postConnectRedirect: string };
|
|
220
249
|
inventoryTimeoutMs?: number;
|
|
221
250
|
jobs?: JobsAdapter;
|
|
@@ -2266,11 +2295,24 @@ export class ChannelConnectorService {
|
|
|
2266
2295
|
return Ok(redactStore(store));
|
|
2267
2296
|
}
|
|
2268
2297
|
|
|
2269
|
-
async listStores(orgId: string): Promise<PluginResult<PublicConnectedStore[]>> {
|
|
2270
|
-
const
|
|
2271
|
-
.
|
|
2272
|
-
|
|
2273
|
-
|
|
2298
|
+
async listStores(orgId: string, context?: StoreReadContext): Promise<PluginResult<PublicConnectedStore[]>> {
|
|
2299
|
+
const allowed = this.options.confineStoreReads
|
|
2300
|
+
? await this.options.confineStoreReads(context ?? { orgId, actor: null, raw: undefined })
|
|
2301
|
+
: null;
|
|
2302
|
+
// An empty allow-list means the caller may read NOTHING, stated here rather than left to the
|
|
2303
|
+
// query builder.
|
|
2304
|
+
//
|
|
2305
|
+
// MEASURED, because the first version of this comment claimed the guard was load-bearing and it
|
|
2306
|
+
// is not: removing this line leaves every row in `confine-store-reads.test.ts` green, so drizzle
|
|
2307
|
+
// already turns `inArray(id, [])` into a predicate that matches nothing. The guard is therefore
|
|
2308
|
+
// the CONTRACT rather than the rescue — `[]` means none, whatever the builder does with an empty
|
|
2309
|
+
// array on some future dialect or version. Worth keeping for that reason and not worth claiming
|
|
2310
|
+
// more for: the row that covers this case is really watching drizzle, not this line.
|
|
2311
|
+
if (allowed !== null && allowed.length === 0) return Ok([]);
|
|
2312
|
+
const predicate = allowed === null
|
|
2313
|
+
? eq(connectedStores.organizationId, orgId)
|
|
2314
|
+
: and(eq(connectedStores.organizationId, orgId), inArray(connectedStores.id, [...allowed]));
|
|
2315
|
+
const rows = await this.db.select().from(connectedStores).where(predicate);
|
|
2274
2316
|
return Ok((rows as ConnectedStore[]).map(redactStore));
|
|
2275
2317
|
}
|
|
2276
2318
|
|