@dartsee/plugin-sdk 0.0.0-stage → 0.0.3
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 +31 -2
- package/dist/client.d.ts +19 -0
- package/dist/errors.d.ts +5 -0
- package/dist/index.d.ts +7 -0
- package/dist/manifest.d.ts +29 -0
- package/dist/plugin-sdk.es.js +118 -0
- package/dist/plugin-sdk.iife.js +3 -0
- package/dist/protocol.d.ts +61 -0
- package/package.json +37 -4
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dartsee/plugin-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SDK for **Dartsee Game Controller plugins**. A plugin is your own website, opened by the Game Controller on
|
|
4
|
+
the lane tablet in a sandboxed iframe. This package is the only channel to the host.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { connect, DartseeError } from '@dartsee/plugin-sdk'
|
|
8
|
+
|
|
9
|
+
const dartsee = await connect()
|
|
10
|
+
const session = await dartsee.session.get()
|
|
11
|
+
const off = dartsee.on('gameEnd', (game) => { /* ... */ })
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Your page runs with an opaque origin
|
|
15
|
+
|
|
16
|
+
The iframe is `sandbox="allow-scripts allow-forms"`, without `allow-same-origin`. That means:
|
|
17
|
+
|
|
18
|
+
- **Your own assets and API are cross-origin to your page.** Serve scripts, fonts and API responses with
|
|
19
|
+
`Access-Control-Allow-Origin: *` (or `null`); accept `Origin: null` on your API. A default Vite build
|
|
20
|
+
(`<script type="module" crossorigin>`) won't load without it.
|
|
21
|
+
- **No device storage.** `localStorage`, `sessionStorage`, IndexedDB, cookies throw `SecurityError`; libraries
|
|
22
|
+
that touch them at startup will crash. Authenticate your backend with the bearer token from
|
|
23
|
+
`auth.getToken()`.
|
|
24
|
+
- **No popups, no top-level navigation**, so no OAuth redirect/popup login.
|
|
25
|
+
- Allow framing: `Content-Security-Policy: frame-ancestors http://127.0.0.1:8080 https://*.dartsee.com`,
|
|
26
|
+
and no `X-Frame-Options`.
|
|
27
|
+
- Target **Chromium 116**, touch only.
|
|
28
|
+
|
|
29
|
+
## Errors
|
|
30
|
+
|
|
31
|
+
Every call rejects with `DartseeError`, `code` one of `TIMEOUT`, `PERMISSION_DENIED`, `UNSUPPORTED`
|
|
32
|
+
(older host — check `dartsee.context.capabilities`), `OFFLINE`, `INVALID_ARGS`, `HOST_ERROR`.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context, EventMap, EventName, GameInfo, SessionInfo } from './protocol';
|
|
2
|
+
export interface CallOptions {
|
|
3
|
+
/** ms */
|
|
4
|
+
timeout?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Dartsee {
|
|
7
|
+
readonly context: Context;
|
|
8
|
+
session: {
|
|
9
|
+
get(opts?: CallOptions): Promise<SessionInfo>;
|
|
10
|
+
};
|
|
11
|
+
game: {
|
|
12
|
+
get(opts?: CallOptions): Promise<GameInfo>;
|
|
13
|
+
};
|
|
14
|
+
auth: {
|
|
15
|
+
getToken(opts?: CallOptions): Promise<string>;
|
|
16
|
+
};
|
|
17
|
+
on<E extends EventName>(event: E, fn: (payload: EventMap[E]) => void): () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function connect({ timeout }?: CallOptions): Promise<Dartsee>;
|
package/dist/errors.d.ts
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { connect } from './client';
|
|
2
|
+
export type { CallOptions, Dartsee } from './client';
|
|
3
|
+
export { DartseeError } from './errors';
|
|
4
|
+
export type { ErrorCode } from './errors';
|
|
5
|
+
export { validateManifest, SIZES } from './manifest';
|
|
6
|
+
export type { ConfigField, Manifest, Size } from './manifest';
|
|
7
|
+
export * from './protocol';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type Size = 'dialog' | 'side' | 'full';
|
|
2
|
+
export declare const SIZES: readonly Size[];
|
|
3
|
+
export interface ConfigField {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
secret?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface Manifest {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
icon: string;
|
|
13
|
+
label: {
|
|
14
|
+
en: string;
|
|
15
|
+
} & Record<string, string>;
|
|
16
|
+
entryUrl: string;
|
|
17
|
+
scopes: string[];
|
|
18
|
+
/** iframe `allow=` features */
|
|
19
|
+
allow?: string[];
|
|
20
|
+
size: Size;
|
|
21
|
+
config?: ConfigField[];
|
|
22
|
+
/** runs from session start, hidden while the overlay is closed */
|
|
23
|
+
background?: boolean;
|
|
24
|
+
/** link-code ownership check at opt-in */
|
|
25
|
+
verifyUrl?: string;
|
|
26
|
+
support: string;
|
|
27
|
+
privacyUrl: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function validateManifest(input: unknown): Manifest;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
class g extends Error {
|
|
2
|
+
constructor(e, i) {
|
|
3
|
+
super(i), this.code = e, this.name = "DartseeError";
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
const N = "CONNECT", b = "READY", A = "EVENT", m = {
|
|
7
|
+
SESSION_GET: "SESSION_GET",
|
|
8
|
+
GAME_GET: "GAME_GET",
|
|
9
|
+
AUTH_GET_TOKEN: "AUTH_GET_TOKEN"
|
|
10
|
+
}, M = Object.values(m), $ = (r, e) => `${r}_RESPONSE_${e}`, k = (r, e) => `${r}_ERROR_${e}`, _ = 1e4, G = 3e4;
|
|
11
|
+
function I({ timeout: r = _ } = {}) {
|
|
12
|
+
const e = window.parent;
|
|
13
|
+
if (e === window)
|
|
14
|
+
return Promise.reject(new g("HOST_ERROR", "Not inside the Dartsee host (no parent frame)"));
|
|
15
|
+
const i = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map();
|
|
16
|
+
let E = 0;
|
|
17
|
+
const o = (n, d, s = {}) => {
|
|
18
|
+
if (!d.capabilities.includes(n))
|
|
19
|
+
return Promise.reject(new g("UNSUPPORTED", `Host ${d.hostVersion} does not support ${n}`));
|
|
20
|
+
const u = s.timeout ?? _, c = String(++E), p = $(n, c), a = k(n, c);
|
|
21
|
+
return new Promise((T, y) => {
|
|
22
|
+
const f = () => {
|
|
23
|
+
clearTimeout(h), i.delete(p), i.delete(a);
|
|
24
|
+
}, h = setTimeout(() => {
|
|
25
|
+
f(), y(new g("TIMEOUT", `${n} timed out after ${u} ms`));
|
|
26
|
+
}, u);
|
|
27
|
+
i.set(p, (R) => {
|
|
28
|
+
f(), T(R.data);
|
|
29
|
+
}), i.set(a, (R) => {
|
|
30
|
+
f();
|
|
31
|
+
const S = R.data;
|
|
32
|
+
y(new g(S?.code ?? "HOST_ERROR", S?.message ?? `${n} failed`));
|
|
33
|
+
}), e.postMessage({ id: n, nonce: c }, "*");
|
|
34
|
+
});
|
|
35
|
+
}, v = (n) => {
|
|
36
|
+
let d;
|
|
37
|
+
return {
|
|
38
|
+
context: n,
|
|
39
|
+
session: { get: (s) => o(m.SESSION_GET, n, s) },
|
|
40
|
+
game: { get: (s) => o(m.GAME_GET, n, s) },
|
|
41
|
+
auth: {
|
|
42
|
+
async getToken(s) {
|
|
43
|
+
return (!d || d.expiresAt - Date.now() < G) && (d = await o(m.AUTH_GET_TOKEN, n, s)), d.token;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
on(s, u) {
|
|
47
|
+
const c = t.get(s) ?? /* @__PURE__ */ new Set();
|
|
48
|
+
return t.set(s, c), c.add(u), () => c.delete(u);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
return new Promise((n, d) => {
|
|
53
|
+
let s = !1;
|
|
54
|
+
const u = setTimeout(() => {
|
|
55
|
+
window.removeEventListener("message", c), d(new g("TIMEOUT", `Host did not answer within ${r} ms`));
|
|
56
|
+
}, r);
|
|
57
|
+
function c(p) {
|
|
58
|
+
if (p.source !== e)
|
|
59
|
+
return;
|
|
60
|
+
const a = p.data;
|
|
61
|
+
if (!(!a || typeof a.id != "string")) {
|
|
62
|
+
if (!s) {
|
|
63
|
+
if (a.id !== b)
|
|
64
|
+
return;
|
|
65
|
+
s = !0, clearTimeout(u), n(v(a.data));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (a.id === A) {
|
|
69
|
+
const { name: T, payload: y } = a.data ?? {};
|
|
70
|
+
t.get(T)?.forEach((f) => {
|
|
71
|
+
try {
|
|
72
|
+
f(y);
|
|
73
|
+
} catch (h) {
|
|
74
|
+
console.error(`[dartsee] ${T} listener threw`, h);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
i.get(a.id)?.(a);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
window.addEventListener("message", c), e.postMessage({ id: N }, "*");
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const O = ["dialog", "side", "full"], l = (r) => typeof r == "string" && r.trim() !== "", w = (r) => {
|
|
86
|
+
try {
|
|
87
|
+
return l(r) && new URL(r).protocol === "https:";
|
|
88
|
+
} catch {
|
|
89
|
+
return !1;
|
|
90
|
+
}
|
|
91
|
+
}, U = (r) => Array.isArray(r) && r.every(l);
|
|
92
|
+
function D(r) {
|
|
93
|
+
const e = r ?? {}, i = [], t = (E, o) => E || i.push(o);
|
|
94
|
+
if (t(l(e.id) && /^[a-z0-9][a-z0-9-]*$/.test(e.id), "id: lowercase letters, digits and dashes"), t(l(e.name), "name: required"), t(l(e.description), "description: required"), t(w(e.icon), "icon: https URL"), t(l(e.label?.en), "label.en: required"), t(e.label == null || Object.values(e.label).every(l), "label: every value a non-empty string"), t(w(e.entryUrl), "entryUrl: https URL"), t(U(e.scopes), "scopes: array of strings"), t(e.allow === void 0 || U(e.allow), "allow: array of strings"), t(O.includes(e.size), `size: one of ${O.join(", ")}`), t(e.background === void 0 || typeof e.background == "boolean", "background: boolean"), t(e.verifyUrl === void 0 || w(e.verifyUrl), "verifyUrl: https URL"), t(l(e.support), "support: required"), t(w(e.privacyUrl), "privacyUrl: https URL"), e.config !== void 0) {
|
|
95
|
+
const E = Array.isArray(e.config) && e.config.every(
|
|
96
|
+
(o) => l(o?.key) && l(o?.label) && (o.secret === void 0 || typeof o.secret == "boolean")
|
|
97
|
+
);
|
|
98
|
+
t(E, "config: array of { key, label, secret? }"), t(E && new Set(e.config.map((o) => o.key)).size === e.config.length, "config: duplicate key");
|
|
99
|
+
}
|
|
100
|
+
if (i.length)
|
|
101
|
+
throw new Error(`Invalid manifest:
|
|
102
|
+
- ${i.join(`
|
|
103
|
+
- `)}`);
|
|
104
|
+
return e;
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
N as CONNECT,
|
|
108
|
+
g as DartseeError,
|
|
109
|
+
A as EVENT,
|
|
110
|
+
b as READY,
|
|
111
|
+
m as RPC,
|
|
112
|
+
M as RPC_IDS,
|
|
113
|
+
O as SIZES,
|
|
114
|
+
I as connect,
|
|
115
|
+
k as errorId,
|
|
116
|
+
$ as responseId,
|
|
117
|
+
D as validateManifest
|
|
118
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var DartseeSDK=function(s){"use strict";class f extends Error{constructor(e,a){super(a),this.code=e,this.name="DartseeError"}}const _="CONNECT",v="READY",U="EVENT",g={SESSION_GET:"SESSION_GET",GAME_GET:"GAME_GET",AUTH_GET_TOKEN:"AUTH_GET_TOKEN"},D=Object.values(g),N=(r,e)=>`${r}_RESPONSE_${e}`,b=(r,e)=>`${r}_ERROR_${e}`,A=1e4,$=3e4;function k({timeout:r=A}={}){const e=window.parent;if(e===window)return Promise.reject(new f("HOST_ERROR","Not inside the Dartsee host (no parent frame)"));const a=new Map,t=new Map;let T=0;const i=(n,E,o={})=>{if(!E.capabilities.includes(n))return Promise.reject(new f("UNSUPPORTED",`Host ${E.hostVersion} does not support ${n}`));const u=o.timeout??A,d=String(++T),y=N(n,d),c=b(n,d);return new Promise((p,S)=>{const w=()=>{clearTimeout(h),a.delete(y),a.delete(c)},h=setTimeout(()=>{w(),S(new f("TIMEOUT",`${n} timed out after ${u} ms`))},u);a.set(y,O=>{w(),p(O.data)}),a.set(c,O=>{w();const M=O.data;S(new f(M?.code??"HOST_ERROR",M?.message??`${n} failed`))}),e.postMessage({id:n,nonce:d},"*")})},P=n=>{let E;return{context:n,session:{get:o=>i(g.SESSION_GET,n,o)},game:{get:o=>i(g.GAME_GET,n,o)},auth:{async getToken(o){return(!E||E.expiresAt-Date.now()<$)&&(E=await i(g.AUTH_GET_TOKEN,n,o)),E.token}},on(o,u){const d=t.get(o)??new Set;return t.set(o,d),d.add(u),()=>d.delete(u)}}};return new Promise((n,E)=>{let o=!1;const u=setTimeout(()=>{window.removeEventListener("message",d),E(new f("TIMEOUT",`Host did not answer within ${r} ms`))},r);function d(y){if(y.source!==e)return;const c=y.data;if(!(!c||typeof c.id!="string")){if(!o){if(c.id!==v)return;o=!0,clearTimeout(u),n(P(c.data));return}if(c.id===U){const{name:p,payload:S}=c.data??{};t.get(p)?.forEach(w=>{try{w(S)}catch(h){console.error(`[dartsee] ${p} listener threw`,h)}});return}a.get(c.id)?.(c)}}window.addEventListener("message",d),e.postMessage({id:_},"*")})}const R=["dialog","side","full"],l=r=>typeof r=="string"&&r.trim()!=="",m=r=>{try{return l(r)&&new URL(r).protocol==="https:"}catch{return!1}},I=r=>Array.isArray(r)&&r.every(l);function G(r){const e=r??{},a=[],t=(T,i)=>T||a.push(i);if(t(l(e.id)&&/^[a-z0-9][a-z0-9-]*$/.test(e.id),"id: lowercase letters, digits and dashes"),t(l(e.name),"name: required"),t(l(e.description),"description: required"),t(m(e.icon),"icon: https URL"),t(l(e.label?.en),"label.en: required"),t(e.label==null||Object.values(e.label).every(l),"label: every value a non-empty string"),t(m(e.entryUrl),"entryUrl: https URL"),t(I(e.scopes),"scopes: array of strings"),t(e.allow===void 0||I(e.allow),"allow: array of strings"),t(R.includes(e.size),`size: one of ${R.join(", ")}`),t(e.background===void 0||typeof e.background=="boolean","background: boolean"),t(e.verifyUrl===void 0||m(e.verifyUrl),"verifyUrl: https URL"),t(l(e.support),"support: required"),t(m(e.privacyUrl),"privacyUrl: https URL"),e.config!==void 0){const T=Array.isArray(e.config)&&e.config.every(i=>l(i?.key)&&l(i?.label)&&(i.secret===void 0||typeof i.secret=="boolean"));t(T,"config: array of { key, label, secret? }"),t(T&&new Set(e.config.map(i=>i.key)).size===e.config.length,"config: duplicate key")}if(a.length)throw new Error(`Invalid manifest:
|
|
2
|
+
- ${a.join(`
|
|
3
|
+
- `)}`);return e}return s.CONNECT=_,s.DartseeError=f,s.EVENT=U,s.READY=v,s.RPC=g,s.RPC_IDS=D,s.SIZES=R,s.connect=k,s.errorId=b,s.responseId=N,s.validateManifest=G,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"}),s}({});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ErrorCode } from './errors';
|
|
2
|
+
import type { Size } from './manifest';
|
|
3
|
+
export interface Message<T = unknown> {
|
|
4
|
+
id: string;
|
|
5
|
+
data?: T;
|
|
6
|
+
nonce?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const CONNECT = "CONNECT";
|
|
9
|
+
export declare const READY = "READY";
|
|
10
|
+
export declare const EVENT = "EVENT";
|
|
11
|
+
export declare const RPC: {
|
|
12
|
+
readonly SESSION_GET: "SESSION_GET";
|
|
13
|
+
readonly GAME_GET: "GAME_GET";
|
|
14
|
+
readonly AUTH_GET_TOKEN: "AUTH_GET_TOKEN";
|
|
15
|
+
};
|
|
16
|
+
export type RpcId = (typeof RPC)[keyof typeof RPC];
|
|
17
|
+
export declare const RPC_IDS: readonly RpcId[];
|
|
18
|
+
export declare const responseId: (id: string, nonce: string) => string;
|
|
19
|
+
export declare const errorId: (id: string, nonce: string) => string;
|
|
20
|
+
export interface ErrorPayload {
|
|
21
|
+
code: ErrorCode;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
export interface Context {
|
|
25
|
+
pluginId: string;
|
|
26
|
+
venueId: string;
|
|
27
|
+
machineId: string;
|
|
28
|
+
language: string;
|
|
29
|
+
size: Size;
|
|
30
|
+
scopes: string[];
|
|
31
|
+
hostVersion: string;
|
|
32
|
+
capabilities: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface SessionInfo {
|
|
35
|
+
active: boolean;
|
|
36
|
+
controlMode: string;
|
|
37
|
+
endsAt?: number;
|
|
38
|
+
playerCount: number;
|
|
39
|
+
}
|
|
40
|
+
export interface GameInfo {
|
|
41
|
+
status: 'no_game' | 'in_game' | 'finished';
|
|
42
|
+
gameType?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface TokenInfo {
|
|
45
|
+
token: string;
|
|
46
|
+
expiresAt: number;
|
|
47
|
+
}
|
|
48
|
+
export interface EventMap {
|
|
49
|
+
gameStart: GameInfo;
|
|
50
|
+
gameEnd: GameInfo;
|
|
51
|
+
sessionStart: SessionInfo;
|
|
52
|
+
sessionEnd: SessionInfo;
|
|
53
|
+
languageChange: {
|
|
54
|
+
language: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export type EventName = keyof EventMap;
|
|
58
|
+
export interface EventMessage<E extends EventName = EventName> {
|
|
59
|
+
name: E;
|
|
60
|
+
payload: EventMap[E];
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dartsee/plugin-sdk",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "SDK for Dartsee Game Controller plugins",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://apidocs.dartsee.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://gitlab.com/dartsee/games-monorepo.git",
|
|
10
|
+
"directory": "packages/plugin-sdk"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/plugin-sdk.es.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"unpkg": "./dist/plugin-sdk.iife.js",
|
|
16
|
+
"jsdelivr": "./dist/plugin-sdk.iife.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/plugin-sdk.es.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "vite build",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"coverage": "vitest run"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"@dartsee:registry": "https://registry.npmjs.org",
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"vite-plugin-dts": "^2.3.0"
|
|
38
|
+
}
|
|
39
|
+
}
|