@owox/plugin-sdk 0.30.1

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 ADDED
@@ -0,0 +1,48 @@
1
+ # @owox/plugin-sdk
2
+
3
+ Build a plugin that runs inside OWOX Data Marts.
4
+
5
+ ```ts
6
+ import { connect } from '@owox/plugin-sdk';
7
+
8
+ const ctx = await connect();
9
+ const dataMarts = await ctx.owox.dataMarts.list();
10
+ ```
11
+
12
+ `connect()` completes a handshake with the OWOX host page and returns a context
13
+ carrying `ctx.owox` — a real OWOX API client.
14
+
15
+ ## What to know before you build
16
+
17
+ Your plugin runs in a cross-origin iframe with an **opaque origin**. That means no
18
+ cookies, no `localStorage`, no `IndexedDB`, no service workers, and requests to your own
19
+ backend arrive with `Origin: null`, so it must send `Access-Control-Allow-Origin: *` and
20
+ cannot use cookie sessions.
21
+
22
+ The same applies to **your own assets**: an opaque origin matches nothing, not even the
23
+ server that delivered the page, so a bundled `<script type="module">` is fetched in CORS
24
+ mode and needs that header too. Without it the page loads and runs no code at all — the
25
+ failure looks like a plugin that does nothing rather than one that could not start.
26
+ GitHub Pages sends the header; a plain static server usually does not.
27
+
28
+ Your entry page must **not** send `X-Frame-Options` or a restrictive
29
+ `Content-Security-Policy: frame-ancestors`, or OWOX will refuse to publish it.
30
+
31
+ `connect()` and the host agree on a protocol version during the handshake, so a page built
32
+ against an SDK the deployment cannot speak fails to start rather than misbehaving.
33
+
34
+ Your plugin never holds a credential. `ctx.owox` calls are brokered by the host page,
35
+ which attaches the token — so requests act with **the authority of the member who
36
+ installed your plugin**, and never more. Do not assume you are trusted beyond that.
37
+
38
+ ## Context
39
+
40
+ | | |
41
+ | ------------------------------------------ | ------------------------------------------------------------------- |
42
+ | `ctx.owox` | OWOX API client. The SDK owns its transport; you cannot replace it. |
43
+ | `ctx.ui.openExternal(url)` | Ask the host to open an external https URL in a new tab. |
44
+ | `ctx.ui.navigate(path)` | Ask the host to go to a page inside OWOX, in place of your frame. |
45
+ | `ctx.signal` | Aborts when the host tears your plugin down. |
46
+ | `ctx.userId`, `ctx.projectId`, `ctx.theme` | Display context. No tokens. |
47
+
48
+ Requests time out after 30 seconds; streamed reads do not. At most 32 may be in flight.
@@ -0,0 +1,19 @@
1
+ import type { OWOXTransport } from '@owox/api-client';
2
+ import type { PluginErrorPayload } from './protocol.js';
3
+ export declare class PluginTransportError extends Error {
4
+ readonly payload: PluginErrorPayload;
5
+ constructor(payload: PluginErrorPayload);
6
+ }
7
+ /**
8
+ * Forwards every call to the host over a MessagePort.
9
+ *
10
+ * The plugin holds no credential and issues no request to OWOX. It holds one end of a
11
+ * channel and can only ask the host to make calls the host has already decided are
12
+ * allowed -- so the worst a compromised plugin can do is ask for something and be
13
+ * refused.
14
+ *
15
+ * Not exported from either package entry point. Plugin code cannot reach this class,
16
+ * cannot construct one, and cannot swap the port underneath it.
17
+ */
18
+ export declare function createIframeTransport(port: MessagePort): OWOXTransport;
19
+ //# sourceMappingURL=iframe-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iframe-transport.d.ts","sourceRoot":"","sources":["../src/iframe-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EACV,kBAAkB,EAInB,MAAM,eAAe,CAAC;AAevB,qBAAa,oBAAqB,SAAQ,KAAK;IACjC,QAAQ,CAAC,OAAO,EAAE,kBAAkB;gBAA3B,OAAO,EAAE,kBAAkB;CAIjD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,GAAG,aAAa,CA0FtE"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Matches the shared axios timeout in the OWOX web app rather than inventing another.
3
+ * A streamed response drops this timer once its head arrives, because NDJSON
4
+ * traversals legitimately run for minutes.
5
+ */
6
+ const REQUEST_TIMEOUT_MS = 30_000;
7
+ export class PluginTransportError extends Error {
8
+ payload;
9
+ constructor(payload) {
10
+ super(payload.message);
11
+ this.payload = payload;
12
+ this.name = 'PluginTransportError';
13
+ }
14
+ }
15
+ /**
16
+ * Forwards every call to the host over a MessagePort.
17
+ *
18
+ * The plugin holds no credential and issues no request to OWOX. It holds one end of a
19
+ * channel and can only ask the host to make calls the host has already decided are
20
+ * allowed -- so the worst a compromised plugin can do is ask for something and be
21
+ * refused.
22
+ *
23
+ * Not exported from either package entry point. Plugin code cannot reach this class,
24
+ * cannot construct one, and cannot swap the port underneath it.
25
+ */
26
+ export function createIframeTransport(port) {
27
+ const pending = new Map();
28
+ port.onmessage = (event) => {
29
+ const response = event.data;
30
+ const waiting = pending.get(response?.id);
31
+ if (!waiting) {
32
+ // An unknown id is either a duplicate of something already settled or noise.
33
+ // Dropping it silently is the only safe reading.
34
+ return;
35
+ }
36
+ pending.delete(response.id);
37
+ if (waiting.timer) {
38
+ clearTimeout(waiting.timer);
39
+ }
40
+ waiting.resolve(response);
41
+ };
42
+ function send(request) {
43
+ // Generated here, inside the closure: a plugin author never sees a correlation id
44
+ // and so cannot address someone else's in-flight request.
45
+ const id = crypto.randomUUID();
46
+ return new Promise((resolve, reject) => {
47
+ const isStream = 'stream' in request && request.stream === true;
48
+ const timer = isStream
49
+ ? null
50
+ : setTimeout(() => {
51
+ pending.delete(id);
52
+ reject(new PluginTransportError({ code: 'TIMEOUT', message: 'The host did not answer' }));
53
+ }, REQUEST_TIMEOUT_MS);
54
+ pending.set(id, { resolve, reject, timer });
55
+ port.postMessage({ ...request, id });
56
+ });
57
+ }
58
+ async function json(request) {
59
+ const response = await send(request);
60
+ if (!response.ok) {
61
+ throw new PluginTransportError(response.error);
62
+ }
63
+ return ('body' in response ? response.body : undefined);
64
+ }
65
+ return {
66
+ getJson: (path, query) => json({ kind: 'api', method: 'GET', path, query: query && Object.entries(query) }),
67
+ postJson: (path, jsonBody, accept) => json({ kind: 'api', method: 'POST', path, body: jsonBody, accept }),
68
+ putJson: (path, jsonBody) => json({ kind: 'api', method: 'PUT', path, body: jsonBody }),
69
+ async getStream(path, query) {
70
+ const response = await send({
71
+ kind: 'api',
72
+ method: 'GET',
73
+ path,
74
+ // Pairs, not an object: `?column=a&column=b` is how the API client asks for two
75
+ // columns, and `Object.fromEntries` would keep only the last one.
76
+ query: query && [...query],
77
+ stream: true,
78
+ });
79
+ if (!response.ok) {
80
+ throw new PluginTransportError(response.error);
81
+ }
82
+ if (!('stream' in response)) {
83
+ throw new PluginTransportError({
84
+ code: 'PROTOCOL_ERROR',
85
+ message: 'The host answered a stream request without a stream',
86
+ });
87
+ }
88
+ // Rebuilt into a Response so the existing NDJSON traversal code works unchanged,
89
+ // including the run-id header it reads.
90
+ return new Response(response.stream, {
91
+ status: response.status,
92
+ headers: response.headers,
93
+ });
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,52 @@
1
+ import { OWOXApiClient } from '@owox/api-client';
2
+ import { type PluginHostContext } from './protocol.js';
3
+ export { PLUGIN_PROTOCOL_VERSION } from './protocol.js';
4
+ export type { PluginErrorCode, PluginErrorPayload, PluginHostContext } from './protocol.js';
5
+ /** Host-mediated actions. Each is a request; the host validates and decides. */
6
+ export interface PluginUi {
7
+ /** Opens an external URL in a new tab, because the sandbox denies you popups. */
8
+ openExternal(url: string): Promise<void>;
9
+ /**
10
+ * Goes to a page inside OWOX Data Marts, in place of this plugin.
11
+ *
12
+ * `path` is absolute within the app, e.g. `/ui/${ctx.projectId}/data-marts/${id}`. The
13
+ * host refuses anything that resolves off its own origin, so this cannot become a way
14
+ * out of the app -- and your frame is unmounted when it does navigate.
15
+ */
16
+ navigate(path: string): void;
17
+ }
18
+ export interface PluginContext extends PluginHostContext {
19
+ /**
20
+ * A real OWOX API client whose transport is owned by this SDK.
21
+ *
22
+ * Plugin code cannot construct, replace or inspect that transport, and no token ever
23
+ * enters this document -- every call is brokered by the trusted host page.
24
+ */
25
+ readonly owox: OWOXApiClient;
26
+ /**
27
+ * Things the sandbox forbids you, which the host will do on your behalf if it agrees.
28
+ *
29
+ * Grouped rather than spread across the context: each one is a request the host is free
30
+ * to refuse, which is a different relationship from `owox`, where you are the one acting.
31
+ */
32
+ readonly ui: PluginUi;
33
+ /** Aborts when the host tears this plugin down. */
34
+ readonly signal: AbortSignal;
35
+ }
36
+ /**
37
+ * Completes the host handshake and returns the plugin context.
38
+ *
39
+ * Idempotent: repeated calls return the same promise, so a plugin cannot open a second
40
+ * channel by calling twice.
41
+ */
42
+ export declare function connect(): Promise<PluginContext>;
43
+ /**
44
+ * Test seam only: the module-level binding is deliberately not resettable in production.
45
+ *
46
+ * Detaches any in-flight handshake as well. A reset that clears the flags but leaves the
47
+ * message listener attached is not a reset -- the stale listener still answers the next
48
+ * host-init, claims the binding, and silently starves the handshake that is actually
49
+ * waiting for it.
50
+ */
51
+ export declare function __resetForTests(): void;
52
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAO5F,gFAAgF;AAChF,MAAM,WAAW,QAAQ;IACvB,iFAAiF;IACjF,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAE7B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IAEtB,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAaD;;;;;GAKG;AACH,wBAAgB,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAGhD;AAuGD;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAItC"}
package/dist/index.js ADDED
@@ -0,0 +1,121 @@
1
+ import { OWOXApiClient } from '@owox/api-client';
2
+ import { createIframeTransport } from './iframe-transport.js';
3
+ import { isHostInit, PLUGIN_PROTOCOL_VERSION, } from './protocol.js';
4
+ export { PLUGIN_PROTOCOL_VERSION } from './protocol.js';
5
+ /** Long enough for a slow host, short enough that a broken embed fails visibly. */
6
+ const HANDSHAKE_TIMEOUT_MS = 10_000;
7
+ /** The host may not be listening yet when we first announce, so keep announcing. */
8
+ const READY_RETRY_MS = 250;
9
+ /**
10
+ * The port, bound exactly once and unreachable from anything this module exports.
11
+ *
12
+ * Module scope rather than a field on the context: a plugin that could reach the port
13
+ * could read every response the host sends, including ones it never asked for.
14
+ */
15
+ let boundPort;
16
+ let connectPromise;
17
+ /** Tears down an in-flight handshake: its listener, its announcer and its deadline. */
18
+ let abandonHandshake;
19
+ /**
20
+ * Completes the host handshake and returns the plugin context.
21
+ *
22
+ * Idempotent: repeated calls return the same promise, so a plugin cannot open a second
23
+ * channel by calling twice.
24
+ */
25
+ export function connect() {
26
+ connectPromise ??= performHandshake();
27
+ return connectPromise;
28
+ }
29
+ function performHandshake() {
30
+ return new Promise((resolve, reject) => {
31
+ // An unframed document has no host. Without this the identity check below is
32
+ // vacuous -- window.parent === window -- and a page could hand itself a channel.
33
+ if (typeof window === 'undefined' || window.parent === window) {
34
+ reject(new Error('This page is not running inside an OWOX plugin frame'));
35
+ return;
36
+ }
37
+ const teardown = new AbortController();
38
+ const announce = () => {
39
+ window.parent.postMessage({ owox: 'plugin-ready', v: PLUGIN_PROTOCOL_VERSION },
40
+ // A concrete target origin never matches an opaque one, so the message would
41
+ // be dropped silently. This carries no data precisely because of that.
42
+ '*');
43
+ };
44
+ const stop = () => {
45
+ window.removeEventListener('message', onMessage);
46
+ clearInterval(announcer);
47
+ clearTimeout(deadline);
48
+ abandonHandshake = undefined;
49
+ };
50
+ abandonHandshake = stop;
51
+ function onMessage(event) {
52
+ // Identity of the sending window, and deliberately nothing about its origin.
53
+ //
54
+ // The opaque origin belongs to *this* document, not to the host: a message coming
55
+ // the other way carries the host's real origin, which this plugin has no way to
56
+ // know and no business demanding. Requiring "null" here refused every genuine host
57
+ // and left connect() timing out -- caught by running the packaged SDK against a
58
+ // real host, because the unit tests delivered host-init as if the host were opaque
59
+ // too.
60
+ if (event.source !== window.parent) {
61
+ return;
62
+ }
63
+ const [port] = event.ports;
64
+ // Exactly one port: a host-init with none has no channel to offer, and one with
65
+ // several is not a shape this protocol defines.
66
+ if (!isHostInit(event.data) || event.ports.length !== 1 || !port) {
67
+ return;
68
+ }
69
+ // Immutable for the document's lifetime: a second host-init, forged or genuine,
70
+ // cannot redirect an already-running plugin onto another channel.
71
+ if (boundPort) {
72
+ return;
73
+ }
74
+ stop();
75
+ resolve(bind(event.data, port, teardown));
76
+ }
77
+ window.addEventListener('message', onMessage);
78
+ announce();
79
+ const announcer = setInterval(announce, READY_RETRY_MS);
80
+ const deadline = setTimeout(() => {
81
+ stop();
82
+ reject(new Error('The OWOX host did not complete the plugin handshake'));
83
+ }, HANDSHAKE_TIMEOUT_MS);
84
+ });
85
+ }
86
+ function bind(init, port, teardown) {
87
+ boundPort = port;
88
+ port.start();
89
+ port.postMessage({ owox: 'plugin-hello', v: PLUGIN_PROTOCOL_VERSION, nonce: init.nonce });
90
+ window.addEventListener('pagehide', () => {
91
+ teardown.abort();
92
+ });
93
+ const owox = new OWOXApiClient({ transport: createIframeTransport(port) });
94
+ return {
95
+ ...init.context,
96
+ owox,
97
+ ui: {
98
+ openExternal: (url) => {
99
+ port.postMessage({ id: crypto.randomUUID(), kind: 'openExternal', url });
100
+ return Promise.resolve();
101
+ },
102
+ navigate: (path) => {
103
+ port.postMessage({ id: crypto.randomUUID(), kind: 'navigate', path });
104
+ },
105
+ },
106
+ signal: teardown.signal,
107
+ };
108
+ }
109
+ /**
110
+ * Test seam only: the module-level binding is deliberately not resettable in production.
111
+ *
112
+ * Detaches any in-flight handshake as well. A reset that clears the flags but leaves the
113
+ * message listener attached is not a reset -- the stale listener still answers the next
114
+ * host-init, claims the binding, and silently starves the handshake that is actually
115
+ * waiting for it.
116
+ */
117
+ export function __resetForTests() {
118
+ abandonHandshake?.();
119
+ boundPort = undefined;
120
+ connectPromise = undefined;
121
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The wire format between an OWOX host page and a plugin running in its iframe.
3
+ *
4
+ * Exported as its own entry point so the host can import these types without pulling in
5
+ * the transport. The transport is not exported from anywhere: that is what makes
6
+ * "plugin code cannot construct or configure it" a property of the package rather than
7
+ * a rule someone has to remember.
8
+ */
9
+ export declare const PLUGIN_PROTOCOL_VERSION: 1;
10
+ /**
11
+ * The plugin's own origin, as the host sees it: a sandboxed frame is opaque, so
12
+ * `event.origin` on a message from the plugin is the literal string "null", and the
13
+ * plugin's outbound postMessage must target '*'.
14
+ *
15
+ * It is a real check in the one direction it applies -- the host requires it of the
16
+ * plugin's announcement. It says nothing in the other direction: the host is served on
17
+ * whatever origin the deployment uses, which the plugin cannot know, so a plugin
18
+ * verifies the host by window identity instead.
19
+ */
20
+ export declare const OPAQUE_ORIGIN = "null";
21
+ /** Announces the plugin is listening. Deliberately carries no data. */
22
+ export interface PluginReadyMessage {
23
+ owox: 'plugin-ready';
24
+ v: typeof PLUGIN_PROTOCOL_VERSION;
25
+ }
26
+ /** Hands over one end of a MessageChannel, which becomes the only data path. */
27
+ export interface PluginHostInitMessage {
28
+ owox: 'host-init';
29
+ v: typeof PLUGIN_PROTOCOL_VERSION;
30
+ nonce: string;
31
+ context: PluginHostContext;
32
+ }
33
+ export interface PluginHelloMessage {
34
+ owox: 'plugin-hello';
35
+ v: typeof PLUGIN_PROTOCOL_VERSION;
36
+ nonce: string;
37
+ }
38
+ /** Ambient information the host chooses to reveal. Display only: no tokens, ever. */
39
+ export interface PluginHostContext {
40
+ readonly pluginId: string;
41
+ readonly installationId: string;
42
+ readonly projectId: string;
43
+ /**
44
+ * The member this plugin is running for.
45
+ *
46
+ * Their name and avatar are deliberately not here: `GET /api/auth/context` already
47
+ * serves both to a plugin that needs them, and a second copy in the handshake would
48
+ * only be one that goes stale.
49
+ */
50
+ readonly userId: string;
51
+ readonly theme: 'light' | 'dark';
52
+ }
53
+ /**
54
+ * Query parameters as ordered pairs rather than an object.
55
+ *
56
+ * A `Record` collapses repeated keys, and repeats are meaningful here: the API client
57
+ * builds `?column=a&column=b` with `URLSearchParams.append`, so flattening would quietly
58
+ * hand a plugin a different dataset than the same call makes outside the iframe.
59
+ */
60
+ export type PluginQuery = readonly (readonly [string, string])[];
61
+ export type PluginRequest = {
62
+ id: string;
63
+ kind: 'api';
64
+ method: 'GET' | 'POST' | 'PUT';
65
+ path: string;
66
+ query?: PluginQuery;
67
+ body?: unknown;
68
+ accept?: string;
69
+ stream?: false;
70
+ } | {
71
+ id: string;
72
+ kind: 'api';
73
+ method: 'GET';
74
+ path: string;
75
+ query?: PluginQuery;
76
+ stream: true;
77
+ } | {
78
+ id: string;
79
+ kind: 'openExternal';
80
+ url: string;
81
+ }
82
+ /**
83
+ * A path inside OWOX, opened in place. Distinct from openExternal on purpose: one
84
+ * leaves the app in a new tab, the other replaces the page the plugin is running on,
85
+ * and the host validates them by different rules.
86
+ */
87
+ | {
88
+ id: string;
89
+ kind: 'navigate';
90
+ path: string;
91
+ };
92
+ /**
93
+ * A request before the transport stamps its correlation id.
94
+ *
95
+ * Distributed on purpose: a plain `Omit` over a union collapses it to the keys every
96
+ * member shares, which would silently erase `method` and `path`. Distribution only
97
+ * happens across a naked generic parameter, so the indirection through `T` is
98
+ * load-bearing.
99
+ */
100
+ type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
101
+ export type PluginRequestInput = DistributiveOmit<PluginRequest, 'id'>;
102
+ export type PluginErrorCode =
103
+ /** The backend answered with a non-2xx status. */
104
+ 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT'
105
+ /** The plugin was suspended deployment-wide while it was open. */
106
+ | 'SUSPENDED'
107
+ /** The host refused before making any request -- see its path and method checks. */
108
+ | 'FORBIDDEN'
109
+ /** Malformed envelope, or too many requests in flight. */
110
+ | 'PROTOCOL_ERROR';
111
+ export interface PluginErrorPayload {
112
+ code: PluginErrorCode;
113
+ status?: number;
114
+ message: string;
115
+ details?: unknown;
116
+ }
117
+ export type PluginResponse = {
118
+ id: string;
119
+ ok: true;
120
+ status: number;
121
+ headers: Record<string, string>;
122
+ body: unknown;
123
+ } | {
124
+ id: string;
125
+ ok: true;
126
+ status: number;
127
+ headers: Record<string, string>;
128
+ /** Transferred rather than copied, so NDJSON traversals stream as they arrive. */
129
+ stream: ReadableStream<Uint8Array>;
130
+ } | {
131
+ id: string;
132
+ ok: false;
133
+ error: PluginErrorPayload;
134
+ };
135
+ export declare function isPluginReady(value: unknown): value is PluginReadyMessage;
136
+ export declare function isPluginHello(value: unknown): value is PluginHelloMessage;
137
+ export declare function isHostInit(value: unknown): value is PluginHostInitMessage;
138
+ export {};
139
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The wire format between an OWOX host page and a plugin running in its iframe.
3
+ *
4
+ * Exported as its own entry point so the host can import these types without pulling in
5
+ * the transport. The transport is not exported from anywhere: that is what makes
6
+ * "plugin code cannot construct or configure it" a property of the package rather than
7
+ * a rule someone has to remember.
8
+ */
9
+ export const PLUGIN_PROTOCOL_VERSION = 1;
10
+ /**
11
+ * The plugin's own origin, as the host sees it: a sandboxed frame is opaque, so
12
+ * `event.origin` on a message from the plugin is the literal string "null", and the
13
+ * plugin's outbound postMessage must target '*'.
14
+ *
15
+ * It is a real check in the one direction it applies -- the host requires it of the
16
+ * plugin's announcement. It says nothing in the other direction: the host is served on
17
+ * whatever origin the deployment uses, which the plugin cannot know, so a plugin
18
+ * verifies the host by window identity instead.
19
+ */
20
+ export const OPAQUE_ORIGIN = 'null';
21
+ export function isPluginReady(value) {
22
+ const message = value;
23
+ return message?.owox === 'plugin-ready' && message.v === PLUGIN_PROTOCOL_VERSION;
24
+ }
25
+ export function isPluginHello(value) {
26
+ const message = value;
27
+ return (message?.owox === 'plugin-hello' &&
28
+ // Checked like the other two guards: without it an ack from a future SDK would be
29
+ // accepted here and then read against this version's rules.
30
+ message.v === PLUGIN_PROTOCOL_VERSION &&
31
+ typeof message.nonce === 'string');
32
+ }
33
+ export function isHostInit(value) {
34
+ const message = value;
35
+ return (message?.owox === 'host-init' &&
36
+ message.v === PLUGIN_PROTOCOL_VERSION &&
37
+ typeof message.nonce === 'string');
38
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@owox/plugin-sdk",
3
+ "version": "0.30.1",
4
+ "description": "SDK for building OWOX Data Marts plugins",
5
+ "type": "module",
6
+ "author": "OWOX",
7
+ "license": "ELv2",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "engines": {
12
+ "node": ">=22.16.0"
13
+ },
14
+ "homepage": "https://github.com/OWOX/owox-data-marts",
15
+ "bugs": "https://github.com/OWOX/owox-data-marts/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/OWOX/owox-data-marts.git",
19
+ "directory": "packages/plugin-sdk"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "prebuild": "npm run build:dep",
24
+ "build:dep": "npm run build -w @owox/api-client --prefix ../..",
25
+ "clean": "shx rm -rf dist",
26
+ "test": "vitest run",
27
+ "lint": "eslint . --config ./eslint.config.js",
28
+ "lint:fix": "eslint . --fix --config ./eslint.config.js",
29
+ "format": "prettier --write \"**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore",
30
+ "format:check": "prettier --check \"**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore",
31
+ "typecheck": "tsc --noEmit",
32
+ "prepublishOnly": "npm run lint && npm run typecheck"
33
+ },
34
+ "keywords": [
35
+ "owox",
36
+ "data-marts",
37
+ "plugin",
38
+ "sdk"
39
+ ],
40
+ "dependencies": {
41
+ "@owox/api-client": "0.30.1"
42
+ },
43
+ "devDependencies": {
44
+ "@owox/eslint-config": "*",
45
+ "@owox/prettier-config": "*",
46
+ "@owox/typescript-config": "*",
47
+ "happy-dom": "^20.0.0",
48
+ "vitest": "^4.1.10"
49
+ },
50
+ "files": [
51
+ "README.md",
52
+ "dist"
53
+ ],
54
+ "exports": {
55
+ ".": {
56
+ "import": "./dist/index.js",
57
+ "types": "./dist/index.d.ts"
58
+ },
59
+ "./protocol": {
60
+ "import": "./dist/protocol.js",
61
+ "types": "./dist/protocol.d.ts"
62
+ }
63
+ },
64
+ "main": "./dist/index.js",
65
+ "types": "./dist/index.d.ts"
66
+ }