@mesyncapp/sdk 0.1.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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/chunk-E3FGRAPR.js +136 -0
- package/dist/chunk-E3FGRAPR.js.map +1 -0
- package/dist/chunk-YPMJ6DJU.js +386 -0
- package/dist/chunk-YPMJ6DJU.js.map +1 -0
- package/dist/elements/index.d.ts +60 -0
- package/dist/elements/index.js +10 -0
- package/dist/elements/index.js.map +1 -0
- package/dist/elements/react.d.ts +54 -0
- package/dist/elements/react.js +72 -0
- package/dist/elements/react.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/types-CRSr6Kwh.d.ts +64 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 senluchen2015
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @mesyncapp/sdk
|
|
2
|
+
|
|
3
|
+
The client library a frontend-only app uses to store its data in the user's
|
|
4
|
+
personal mesync backend instead of `localStorage`. See
|
|
5
|
+
`../docs/DEVELOPER_GUIDE.md` for the full integration guide (registering an
|
|
6
|
+
app, defining tables, migrations, the consent flow, cross-app data) — this
|
|
7
|
+
README only covers the SDK's own API surface. `../docs/DESIGN.md` covers
|
|
8
|
+
the system this talks to.
|
|
9
|
+
|
|
10
|
+
This is intentionally tiny: configure once, redirect for consent, then
|
|
11
|
+
read/write a table. Built with tsup to `dist/`, with three entry points:
|
|
12
|
+
`@mesyncapp/sdk` (core client), `@mesyncapp/sdk/elements` (framework-agnostic
|
|
13
|
+
custom elements), and `@mesyncapp/sdk/elements/react` (React wrappers, only
|
|
14
|
+
needed if you use React).
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
**Registering your app** — a manifest, not a separate dashboard step:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { defineManifest } from "@mesyncapp/sdk";
|
|
22
|
+
|
|
23
|
+
export const manifest = defineManifest({
|
|
24
|
+
uniqueId: "meals", // your choice — check availability first via GET /api/catalog/unique-id-available
|
|
25
|
+
name: "Meals",
|
|
26
|
+
redirectUris: [window.location.origin + "/callback"],
|
|
27
|
+
tables: [{ name: "entries", columns: [{ name: "title", type: "string", required: true, default: "" }] }],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { mesync } from "@mesyncapp/sdk";
|
|
33
|
+
|
|
34
|
+
mesync.configure({
|
|
35
|
+
mesyncUrl: "http://localhost:5173", // the mesync app's origin
|
|
36
|
+
uniqueId: manifest.uniqueId,
|
|
37
|
+
redirectUri: window.location.origin + "/callback",
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**On your "Connect with mesync" button** — pass `manifest` in alongside your
|
|
42
|
+
scopes, there's no separate registration step:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { MesyncConnect } from "@mesyncapp/sdk/elements/react";
|
|
46
|
+
|
|
47
|
+
<MesyncConnect
|
|
48
|
+
scopes={[
|
|
49
|
+
{ namespace: "meals", table: "entries", permission: "write" }, // your own data
|
|
50
|
+
{ namespace: "move", table: "logs", permission: "read", reason: "To show today's workouts." }, // someone else's, with their consent
|
|
51
|
+
]}
|
|
52
|
+
manifest={manifest}
|
|
53
|
+
/>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The SDK itself never decides anything about registration — `manifest` just
|
|
57
|
+
rides along to mesync's own hosted consent screen, which checks who's
|
|
58
|
+
registered/owns what and shows "Register new app", "Update app schema", or
|
|
59
|
+
today's ordinary consent screen accordingly. The very first connect for a
|
|
60
|
+
brand-new app is also its registration — there's no separate setup step,
|
|
61
|
+
and the developer always types their password on mesync's own real login
|
|
62
|
+
page, never a form your app rendered. See `docs/DEVELOPER_GUIDE.md` for the
|
|
63
|
+
full model (including the headless `useMesyncConnect()` equivalent, and
|
|
64
|
+
what happens on a `uniqueId` collision).
|
|
65
|
+
|
|
66
|
+
Calling the underlying client functions directly instead of the element:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
await mesync.login(
|
|
70
|
+
[{ namespace: "meals", table: "entries", permission: "write" }],
|
|
71
|
+
{ manifest } // omit entirely if you've already registered elsewhere
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For direct/scripting registration (e.g. a CI setup script) instead of the
|
|
76
|
+
interactive connect flow, `mesync.registerManifest(manifest, token)` takes
|
|
77
|
+
a bearer token you've already obtained yourself (e.g. your own
|
|
78
|
+
`POST /api/auth/login` call) and does the same create/confirm/update
|
|
79
|
+
upsert `POST /api/apps/manifest` does.
|
|
80
|
+
|
|
81
|
+
`reason` is optional per scope — a short line shown on the consent screen
|
|
82
|
+
explaining why your app wants that specific permission.
|
|
83
|
+
|
|
84
|
+
This redirects the browser to the mesync consent screen. The user approves,
|
|
85
|
+
and is redirected back to your `redirectUri` with a code in the query string.
|
|
86
|
+
|
|
87
|
+
**On your `redirectUri` page**, once on load:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await mesync.handleCallback();
|
|
91
|
+
// tokens are now stored; redirect to wherever your app actually starts
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Anywhere after that**, read and write rows:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const meals = mesync.table("meals", "entries");
|
|
98
|
+
|
|
99
|
+
await meals.insert({ title: "Lunch", calories: 500 });
|
|
100
|
+
const { items, total } = await meals.list();
|
|
101
|
+
await meals.update(items[0].id, { calories: 600 });
|
|
102
|
+
await meals.remove(items[0].id);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`mesync.table()` works the same way for a table you were only granted
|
|
106
|
+
`"read"` on — write calls will just 403.
|
|
107
|
+
|
|
108
|
+
## Notes
|
|
109
|
+
|
|
110
|
+
- `mesyncUrl` is assumed to serve both the consent UI (`/oauth/authorize`)
|
|
111
|
+
and the API (`/api/...`) on the same origin — that's how the dev setup
|
|
112
|
+
(Vite proxying `/api` to the backend) and the likely deployment shape both
|
|
113
|
+
work. If that ever splits into two origins, `configure()` will need a
|
|
114
|
+
second URL.
|
|
115
|
+
- Access tokens are refreshed transparently on a 401 using the stored
|
|
116
|
+
refresh token; if that also fails, calls throw `MesyncAuthError` and the
|
|
117
|
+
app should call `mesync.login(...)` again.
|
|
118
|
+
- Tokens live in `localStorage` under `mesync_access_token` /
|
|
119
|
+
`mesync_refresh_token`. The PKCE verifier and OAuth `state` also live in
|
|
120
|
+
`localStorage` (not `sessionStorage`) — in popup mode the popup navigates
|
|
121
|
+
to `mesyncUrl` and back, and an auxiliary window only shares its opener's
|
|
122
|
+
`sessionStorage` while same-origin, so that round trip can lose it.
|
|
123
|
+
`localStorage` is keyed purely by origin with no such restriction. They're
|
|
124
|
+
deleted immediately after the callback reads them, so still short-lived
|
|
125
|
+
in practice.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mesync
|
|
3
|
+
} from "./chunk-YPMJ6DJU.js";
|
|
4
|
+
|
|
5
|
+
// src/elements/connect-element.ts
|
|
6
|
+
var STYLE = `
|
|
7
|
+
:host { display: inline-block; font-family: inherit; }
|
|
8
|
+
button {
|
|
9
|
+
font: inherit;
|
|
10
|
+
font-size: 0.95rem;
|
|
11
|
+
padding: 0.6rem 1.2rem;
|
|
12
|
+
border-radius: var(--mesync-radius, 8px);
|
|
13
|
+
border: none;
|
|
14
|
+
background: var(--mesync-bg, #111827);
|
|
15
|
+
color: var(--mesync-color, #fff);
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
button:disabled { opacity: 0.6; cursor: default; }
|
|
19
|
+
::slotted(*) { cursor: pointer; }
|
|
20
|
+
.error { color: var(--mesync-error-color, #dc2626); font-size: 0.85rem; margin-top: 0.4rem; }
|
|
21
|
+
`;
|
|
22
|
+
var MesyncConnectElement = class extends HTMLElement {
|
|
23
|
+
#button;
|
|
24
|
+
#slot;
|
|
25
|
+
#fallbackText;
|
|
26
|
+
#errorEl;
|
|
27
|
+
#connecting = false;
|
|
28
|
+
static get observedAttributes() {
|
|
29
|
+
return ["label", "connected-label"];
|
|
30
|
+
}
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
34
|
+
const style = document.createElement("style");
|
|
35
|
+
style.textContent = STYLE;
|
|
36
|
+
this.#button = document.createElement("button");
|
|
37
|
+
this.#slot = document.createElement("slot");
|
|
38
|
+
this.#fallbackText = document.createTextNode("");
|
|
39
|
+
this.#slot.append(this.#fallbackText);
|
|
40
|
+
this.#button.append(this.#slot);
|
|
41
|
+
this.#errorEl = document.createElement("div");
|
|
42
|
+
this.#errorEl.className = "error";
|
|
43
|
+
this.#errorEl.setAttribute("part", "error");
|
|
44
|
+
this.#button.setAttribute("part", "button");
|
|
45
|
+
shadow.append(style, this.#button, this.#errorEl);
|
|
46
|
+
this.#button.addEventListener("click", () => void this.#connect());
|
|
47
|
+
}
|
|
48
|
+
connectedCallback() {
|
|
49
|
+
this.#render();
|
|
50
|
+
}
|
|
51
|
+
attributeChangedCallback() {
|
|
52
|
+
this.#render();
|
|
53
|
+
}
|
|
54
|
+
#scopes() {
|
|
55
|
+
const raw = this.getAttribute("scopes");
|
|
56
|
+
if (!raw) throw new Error("<mesync-connect> requires a scopes attribute (JSON string)");
|
|
57
|
+
return JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
#manifest() {
|
|
60
|
+
const raw = this.getAttribute("manifest");
|
|
61
|
+
return raw ? JSON.parse(raw) : void 0;
|
|
62
|
+
}
|
|
63
|
+
#render() {
|
|
64
|
+
const loggedIn = mesync.isLoggedIn();
|
|
65
|
+
this.#button.disabled = this.#connecting || loggedIn;
|
|
66
|
+
this.#fallbackText.textContent = this.#connecting ? "Connecting\u2026" : loggedIn ? this.getAttribute("connected-label") ?? "Connected" : this.getAttribute("label") ?? "Connect with mesync";
|
|
67
|
+
this.toggleAttribute("connecting", this.#connecting);
|
|
68
|
+
this.toggleAttribute("connected", loggedIn);
|
|
69
|
+
}
|
|
70
|
+
async #connect() {
|
|
71
|
+
this.#connecting = true;
|
|
72
|
+
this.#errorEl.textContent = "";
|
|
73
|
+
this.#render();
|
|
74
|
+
const manifest = this.#manifest();
|
|
75
|
+
const usePopup = this.getAttribute("mode") === "popup";
|
|
76
|
+
const options = manifest ? { manifest } : void 0;
|
|
77
|
+
try {
|
|
78
|
+
if (usePopup) {
|
|
79
|
+
await mesync.loginPopup(this.#scopes(), options);
|
|
80
|
+
this.#connecting = false;
|
|
81
|
+
this.#render();
|
|
82
|
+
this.dispatchEvent(new CustomEvent("mesync-connected", { bubbles: true, composed: true }));
|
|
83
|
+
} else {
|
|
84
|
+
await mesync.login(this.#scopes(), options);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
this.#connecting = false;
|
|
88
|
+
const message = err instanceof Error ? err.message : "Could not connect to mesync.";
|
|
89
|
+
this.#errorEl.textContent = message;
|
|
90
|
+
this.#render();
|
|
91
|
+
this.dispatchEvent(new CustomEvent("mesync-error", { detail: message, bubbles: true, composed: true }));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
if (!customElements.get("mesync-connect")) {
|
|
96
|
+
customElements.define("mesync-connect", MesyncConnectElement);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/elements/callback-element.ts
|
|
100
|
+
var MesyncCallbackElement = class extends HTMLElement {
|
|
101
|
+
#statusEl;
|
|
102
|
+
constructor() {
|
|
103
|
+
super();
|
|
104
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
105
|
+
const style = document.createElement("style");
|
|
106
|
+
style.textContent = `:host { display: block; } p { font-family: inherit; }`;
|
|
107
|
+
this.#statusEl = document.createElement("p");
|
|
108
|
+
this.#statusEl.setAttribute("part", "status");
|
|
109
|
+
this.#statusEl.textContent = "Connecting to mesync\u2026";
|
|
110
|
+
shadow.append(style, this.#statusEl);
|
|
111
|
+
}
|
|
112
|
+
connectedCallback() {
|
|
113
|
+
void this.#run();
|
|
114
|
+
}
|
|
115
|
+
async #run() {
|
|
116
|
+
try {
|
|
117
|
+
await mesync.handleCallback();
|
|
118
|
+
this.dispatchEvent(new CustomEvent("mesync-connected", { bubbles: true, composed: true }));
|
|
119
|
+
const redirectTo = this.getAttribute("redirect-to") ?? "./";
|
|
120
|
+
if (redirectTo) window.location.href = redirectTo;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const message = err instanceof Error ? err.message : "Could not connect to mesync.";
|
|
123
|
+
this.#statusEl.textContent = message;
|
|
124
|
+
this.dispatchEvent(new CustomEvent("mesync-error", { detail: message, bubbles: true, composed: true }));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
if (!customElements.get("mesync-callback")) {
|
|
129
|
+
customElements.define("mesync-callback", MesyncCallbackElement);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
MesyncConnectElement,
|
|
134
|
+
MesyncCallbackElement
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=chunk-E3FGRAPR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/elements/connect-element.ts","../src/elements/callback-element.ts"],"sourcesContent":["import { mesync } from \"../client\";\nimport type { MesyncManifest, ScopeRequest } from \"../types\";\n\nconst STYLE = `\n :host { display: inline-block; font-family: inherit; }\n button {\n font: inherit;\n font-size: 0.95rem;\n padding: 0.6rem 1.2rem;\n border-radius: var(--mesync-radius, 8px);\n border: none;\n background: var(--mesync-bg, #111827);\n color: var(--mesync-color, #fff);\n cursor: pointer;\n }\n button:disabled { opacity: 0.6; cursor: default; }\n ::slotted(*) { cursor: pointer; }\n .error { color: var(--mesync-error-color, #dc2626); font-size: 0.85rem; margin-top: 0.4rem; }\n`;\n\n/**\n * Prebuilt \"Connect with mesync\" button. Framework-agnostic (plain custom\n * element) so it can drop into React/Vue/Svelte/etc without a framework-\n * specific SDK build. Usage:\n *\n * <mesync-connect scopes='[{\"namespace\":\"move\",\"table\":\"logs\",\"permission\":\"write\"}]'></mesync-connect>\n *\n * By default (mode=\"redirect\", or omitted) this navigates the whole page to\n * the consent screen, so completion is observed on the redirect_uri page via\n * <mesync-callback> instead of here. Set mode=\"popup\" to instead drive the\n * consent screen in a popup window and get a \"mesync-connected\" event on\n * this element once it completes, with no page navigation.\n *\n * Pass a `manifest` attribute (JSON, same shape as defineManifest()'s\n * argument) to fold app registration into this same button — it's just\n * forwarded along to mesync's own hosted consent screen, which checks\n * whether `manifest.uniqueId` needs registering, needs a schema update, or\n * is already in sync, and shows the right screen there (including, on the\n * very first-ever connect, a real login page hosted on mesync's own origin —\n * never a form rendered by this element or by your app). This element makes\n * no registration decisions itself.\n *\n * Renders `label`/`connected-label` as plain text by default, but any light-DOM\n * children take over instead — e.g. drop in an icon or custom markup:\n *\n * <mesync-connect scopes='...'><img src=\"/icon.svg\" alt=\"Connect\"></mesync-connect>\n *\n * The host toggles `connecting`/`connected` boolean attributes so custom\n * content can react via CSS (e.g. `mesync-connect[connecting] img { opacity: .5 }`).\n * Styling hooks: `::part(button)`, `::part(error)`, and the CSS custom\n * properties --mesync-bg, --mesync-color, --mesync-radius, --mesync-error-color.\n *\n * Emits \"mesync-error\" if the connection fails (scope resolution before the\n * redirect, a blocked/closed popup, or a rejected popup flow).\n */\nclass MesyncConnectElement extends HTMLElement {\n #button: HTMLButtonElement;\n #slot: HTMLSlotElement;\n #fallbackText: Text;\n #errorEl: HTMLDivElement;\n #connecting = false;\n\n static get observedAttributes(): string[] {\n return [\"label\", \"connected-label\"];\n }\n\n constructor() {\n super();\n const shadow = this.attachShadow({ mode: \"open\" });\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n this.#button = document.createElement(\"button\");\n this.#slot = document.createElement(\"slot\");\n this.#fallbackText = document.createTextNode(\"\");\n // Fallback content (shown only when nothing is slotted in) — lets callers\n // swap in an icon/custom markup as light-DOM children of <mesync-connect>\n // while defaulting to plain text when they don't.\n this.#slot.append(this.#fallbackText);\n this.#button.append(this.#slot);\n this.#errorEl = document.createElement(\"div\");\n this.#errorEl.className = \"error\";\n this.#errorEl.setAttribute(\"part\", \"error\");\n this.#button.setAttribute(\"part\", \"button\");\n shadow.append(style, this.#button, this.#errorEl);\n this.#button.addEventListener(\"click\", () => void this.#connect());\n }\n\n connectedCallback(): void {\n this.#render();\n }\n\n attributeChangedCallback(): void {\n this.#render();\n }\n\n #scopes(): ScopeRequest[] {\n const raw = this.getAttribute(\"scopes\");\n if (!raw) throw new Error(\"<mesync-connect> requires a scopes attribute (JSON string)\");\n return JSON.parse(raw) as ScopeRequest[];\n }\n\n #manifest(): MesyncManifest | undefined {\n const raw = this.getAttribute(\"manifest\");\n return raw ? (JSON.parse(raw) as MesyncManifest) : undefined;\n }\n\n #render(): void {\n const loggedIn = mesync.isLoggedIn();\n this.#button.disabled = this.#connecting || loggedIn;\n this.#fallbackText.textContent = this.#connecting\n ? \"Connecting…\"\n : loggedIn\n ? (this.getAttribute(\"connected-label\") ?? \"Connected\")\n : (this.getAttribute(\"label\") ?? \"Connect with mesync\");\n this.toggleAttribute(\"connecting\", this.#connecting);\n this.toggleAttribute(\"connected\", loggedIn);\n }\n\n async #connect(): Promise<void> {\n this.#connecting = true;\n this.#errorEl.textContent = \"\";\n this.#render();\n const manifest = this.#manifest();\n const usePopup = this.getAttribute(\"mode\") === \"popup\";\n const options = manifest ? { manifest } : undefined;\n try {\n if (usePopup) {\n await mesync.loginPopup(this.#scopes(), options);\n this.#connecting = false;\n this.#render();\n this.dispatchEvent(new CustomEvent(\"mesync-connected\", { bubbles: true, composed: true }));\n } else {\n await mesync.login(this.#scopes(), options);\n // Browser navigates away on success; nothing after this line normally runs.\n }\n } catch (err) {\n this.#connecting = false;\n const message = err instanceof Error ? err.message : \"Could not connect to mesync.\";\n this.#errorEl.textContent = message;\n this.#render();\n this.dispatchEvent(new CustomEvent(\"mesync-error\", { detail: message, bubbles: true, composed: true }));\n }\n }\n}\n\nif (!customElements.get(\"mesync-connect\")) {\n customElements.define(\"mesync-connect\", MesyncConnectElement);\n}\n\nexport { MesyncConnectElement };\n","import { mesync } from \"../client\";\n\n/**\n * Drop-in replacement for the redirect_uri page's callback handler. Calls\n * mesync.handleCallback() on mount, then either redirects (default: back to\n * \"/\") or emits \"mesync-connected\" for the host app to handle itself.\n *\n * <mesync-callback redirect-to=\"./\"></mesync-callback>\n *\n * Set redirect-to=\"\" (empty) to suppress the redirect and just listen for\n * \"mesync-connected\" / \"mesync-error\" instead.\n */\nclass MesyncCallbackElement extends HTMLElement {\n #statusEl: HTMLParagraphElement;\n\n constructor() {\n super();\n const shadow = this.attachShadow({ mode: \"open\" });\n const style = document.createElement(\"style\");\n style.textContent = `:host { display: block; } p { font-family: inherit; }`;\n this.#statusEl = document.createElement(\"p\");\n this.#statusEl.setAttribute(\"part\", \"status\");\n this.#statusEl.textContent = \"Connecting to mesync…\";\n shadow.append(style, this.#statusEl);\n }\n\n connectedCallback(): void {\n void this.#run();\n }\n\n async #run(): Promise<void> {\n try {\n await mesync.handleCallback();\n this.dispatchEvent(new CustomEvent(\"mesync-connected\", { bubbles: true, composed: true }));\n const redirectTo = this.getAttribute(\"redirect-to\") ?? \"./\";\n if (redirectTo) window.location.href = redirectTo;\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Could not connect to mesync.\";\n this.#statusEl.textContent = message;\n this.dispatchEvent(new CustomEvent(\"mesync-error\", { detail: message, bubbles: true, composed: true }));\n }\n }\n}\n\nif (!customElements.get(\"mesync-callback\")) {\n customElements.define(\"mesync-callback\", MesyncCallbackElement);\n}\n\nexport { MesyncCallbackElement };\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// src/pkce.ts
|
|
2
|
+
function base64UrlEncode(bytes) {
|
|
3
|
+
let binary = "";
|
|
4
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
5
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function randomUrlSafeString(byteLength = 32) {
|
|
8
|
+
const bytes = new Uint8Array(byteLength);
|
|
9
|
+
crypto.getRandomValues(bytes);
|
|
10
|
+
return base64UrlEncode(bytes);
|
|
11
|
+
}
|
|
12
|
+
async function pkceChallengeFromVerifier(verifier) {
|
|
13
|
+
const data = new TextEncoder().encode(verifier);
|
|
14
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
15
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/storage.ts
|
|
19
|
+
var ACCESS_TOKEN_KEY = "mesync_access_token";
|
|
20
|
+
var REFRESH_TOKEN_KEY = "mesync_refresh_token";
|
|
21
|
+
var PKCE_VERIFIER_KEY = "mesync_pkce_verifier";
|
|
22
|
+
var OAUTH_STATE_KEY = "mesync_oauth_state";
|
|
23
|
+
var tokenStorage = {
|
|
24
|
+
getAccessToken: () => localStorage.getItem(ACCESS_TOKEN_KEY),
|
|
25
|
+
getRefreshToken: () => localStorage.getItem(REFRESH_TOKEN_KEY),
|
|
26
|
+
setTokens(accessToken, refreshToken) {
|
|
27
|
+
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
|
28
|
+
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
|
29
|
+
},
|
|
30
|
+
clear() {
|
|
31
|
+
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
|
32
|
+
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var pkceStorage = {
|
|
36
|
+
save(verifier, state) {
|
|
37
|
+
localStorage.setItem(PKCE_VERIFIER_KEY, verifier);
|
|
38
|
+
localStorage.setItem(OAUTH_STATE_KEY, state);
|
|
39
|
+
},
|
|
40
|
+
consume() {
|
|
41
|
+
const verifier = localStorage.getItem(PKCE_VERIFIER_KEY);
|
|
42
|
+
const state = localStorage.getItem(OAUTH_STATE_KEY);
|
|
43
|
+
localStorage.removeItem(PKCE_VERIFIER_KEY);
|
|
44
|
+
localStorage.removeItem(OAUTH_STATE_KEY);
|
|
45
|
+
if (!verifier || !state) return null;
|
|
46
|
+
return { verifier, state };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/client.ts
|
|
51
|
+
var MesyncAuthError = class extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "MesyncAuthError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var MesyncUniqueIdConflictError = class extends Error {
|
|
58
|
+
constructor(uniqueId, suggestedUniqueId, message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.uniqueId = uniqueId;
|
|
61
|
+
this.suggestedUniqueId = suggestedUniqueId;
|
|
62
|
+
this.name = "MesyncUniqueIdConflictError";
|
|
63
|
+
}
|
|
64
|
+
uniqueId;
|
|
65
|
+
suggestedUniqueId;
|
|
66
|
+
};
|
|
67
|
+
var MesyncManifestDivergedError = class extends Error {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "MesyncManifestDivergedError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var config = null;
|
|
74
|
+
function requireConfig() {
|
|
75
|
+
if (!config) {
|
|
76
|
+
throw new Error("mesync.configure(...) must be called before using the SDK");
|
|
77
|
+
}
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
function configure(newConfig) {
|
|
81
|
+
config = newConfig;
|
|
82
|
+
}
|
|
83
|
+
async function resolveTableDefId(namespace, table2) {
|
|
84
|
+
const { mesyncUrl } = requireConfig();
|
|
85
|
+
const resp = await fetch(`${mesyncUrl}/api/catalog/apps/${namespace}/tables`);
|
|
86
|
+
if (resp.status === 404) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`App '${namespace}' isn't registered in the mesync catalog yet, so '${namespace}.${table2}' can't be resolved. It needs to complete its own connect flow (with its manifest) at least once before other apps can request cross-app access to its tables.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (!resp.ok) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Could not look up table '${namespace}.${table2}' in the mesync catalog (HTTP ${resp.status})`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const tables = await resp.json();
|
|
97
|
+
const match = tables.find((t) => t.name === table2);
|
|
98
|
+
if (!match) {
|
|
99
|
+
throw new Error(`Table '${table2}' not found under namespace '${namespace}'`);
|
|
100
|
+
}
|
|
101
|
+
return match.table_def_id;
|
|
102
|
+
}
|
|
103
|
+
function isOwnScope(s, uniqueId) {
|
|
104
|
+
return uniqueId !== void 0 && s.namespace === uniqueId;
|
|
105
|
+
}
|
|
106
|
+
async function buildAuthorizeUrl(scopes, manifest) {
|
|
107
|
+
const { mesyncUrl, uniqueId, redirectUri } = requireConfig();
|
|
108
|
+
if (scopes.length === 0) {
|
|
109
|
+
throw new Error("login() requires at least one scope");
|
|
110
|
+
}
|
|
111
|
+
const ownScopes = scopes.filter((s) => isOwnScope(s, manifest?.uniqueId));
|
|
112
|
+
const crossScopes = scopes.filter((s) => !isOwnScope(s, manifest?.uniqueId));
|
|
113
|
+
const crossEntries = await Promise.all(
|
|
114
|
+
crossScopes.map(async (s) => `${await resolveTableDefId(s.namespace, s.table)}:${s.permission}`)
|
|
115
|
+
);
|
|
116
|
+
const verifier = randomUrlSafeString();
|
|
117
|
+
const challenge = await pkceChallengeFromVerifier(verifier);
|
|
118
|
+
const state = randomUrlSafeString(16);
|
|
119
|
+
pkceStorage.save(verifier, state);
|
|
120
|
+
const query = new URLSearchParams({
|
|
121
|
+
// Wire-protocol param name per OAuth2 convention — its value is the app's uniqueId.
|
|
122
|
+
client_id: uniqueId,
|
|
123
|
+
redirect_uri: redirectUri,
|
|
124
|
+
code_challenge: challenge,
|
|
125
|
+
code_challenge_method: "S256",
|
|
126
|
+
state,
|
|
127
|
+
// The app's own origin, so the platform can link back to it once connected.
|
|
128
|
+
app_url: window.location.origin
|
|
129
|
+
});
|
|
130
|
+
if (crossEntries.length) {
|
|
131
|
+
query.set("scope", crossEntries.join(","));
|
|
132
|
+
if (crossScopes.some((s) => s.reason)) {
|
|
133
|
+
query.set("reasons", crossScopes.map((s) => encodeURIComponent(s.reason ?? "")).join(","));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (ownScopes.length) {
|
|
137
|
+
query.set("own_scope", ownScopes.map((s) => `${s.table}:${s.permission}`).join(","));
|
|
138
|
+
if (ownScopes.some((s) => s.reason)) {
|
|
139
|
+
query.set("own_reasons", ownScopes.map((s) => encodeURIComponent(s.reason ?? "")).join(","));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (manifest) {
|
|
143
|
+
query.set(
|
|
144
|
+
"manifest",
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
unique_id: manifest.uniqueId,
|
|
147
|
+
name: manifest.name,
|
|
148
|
+
description: manifest.description,
|
|
149
|
+
redirect_uris: manifest.redirectUris,
|
|
150
|
+
tables: manifest.tables
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return `${mesyncUrl}/oauth/authorize?${query.toString()}`;
|
|
155
|
+
}
|
|
156
|
+
async function login(scopes, options = {}) {
|
|
157
|
+
window.location.href = await buildAuthorizeUrl(scopes, options.manifest);
|
|
158
|
+
}
|
|
159
|
+
function loginPopup(scopes, options = {}) {
|
|
160
|
+
const popup = window.open("", "mesync-connect", "width=480,height=640,menubar=no,toolbar=no,status=no");
|
|
161
|
+
if (!popup) {
|
|
162
|
+
return Promise.reject(
|
|
163
|
+
new MesyncAuthError("Popup blocked \u2014 allow popups for this site, or use mesync.login() instead.")
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
let settled = false;
|
|
168
|
+
function cleanup() {
|
|
169
|
+
window.removeEventListener("message", handleMessage);
|
|
170
|
+
clearInterval(closePoll);
|
|
171
|
+
}
|
|
172
|
+
function handleMessage(e) {
|
|
173
|
+
if (e.source !== popup || e.origin !== window.location.origin) return;
|
|
174
|
+
settled = true;
|
|
175
|
+
cleanup();
|
|
176
|
+
if (e.data?.type === "mesync-connected") {
|
|
177
|
+
resolve();
|
|
178
|
+
} else if (e.data?.type === "mesync-error") {
|
|
179
|
+
reject(new MesyncAuthError(e.data.message ?? "Could not connect to mesync."));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
window.addEventListener("message", handleMessage);
|
|
183
|
+
const closePoll = window.setInterval(() => {
|
|
184
|
+
if (popup.closed && !settled) {
|
|
185
|
+
settled = true;
|
|
186
|
+
cleanup();
|
|
187
|
+
reject(new MesyncAuthError("Connection window was closed before completing."));
|
|
188
|
+
}
|
|
189
|
+
}, 500);
|
|
190
|
+
buildAuthorizeUrl(scopes, options.manifest).then((url) => {
|
|
191
|
+
popup.location.href = url;
|
|
192
|
+
}).catch((err) => {
|
|
193
|
+
if (settled) return;
|
|
194
|
+
settled = true;
|
|
195
|
+
cleanup();
|
|
196
|
+
popup.close();
|
|
197
|
+
reject(err instanceof Error ? err : new MesyncAuthError("Could not connect to mesync."));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function isPopup() {
|
|
202
|
+
return !!window.opener && window.opener !== window;
|
|
203
|
+
}
|
|
204
|
+
function notifyOpenerAndClose(payload) {
|
|
205
|
+
window.opener.postMessage(payload, window.location.origin);
|
|
206
|
+
window.close();
|
|
207
|
+
}
|
|
208
|
+
async function exchangeCodeForTokens() {
|
|
209
|
+
const { mesyncUrl, uniqueId, redirectUri } = requireConfig();
|
|
210
|
+
const params = new URLSearchParams(window.location.search);
|
|
211
|
+
const errorParam = params.get("error");
|
|
212
|
+
if (errorParam) {
|
|
213
|
+
throw new MesyncAuthError(`mesync authorization was denied: ${errorParam}`);
|
|
214
|
+
}
|
|
215
|
+
const code = params.get("code");
|
|
216
|
+
const returnedState = params.get("state");
|
|
217
|
+
if (!code || !returnedState) {
|
|
218
|
+
throw new MesyncAuthError("Missing code/state in callback URL");
|
|
219
|
+
}
|
|
220
|
+
const saved = pkceStorage.consume();
|
|
221
|
+
if (!saved || saved.state !== returnedState) {
|
|
222
|
+
throw new MesyncAuthError("OAuth state mismatch \u2014 possible CSRF, aborting");
|
|
223
|
+
}
|
|
224
|
+
const resp = await fetch(`${mesyncUrl}/api/oauth/token`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
grant_type: "authorization_code",
|
|
229
|
+
code,
|
|
230
|
+
redirect_uri: redirectUri,
|
|
231
|
+
code_verifier: saved.verifier,
|
|
232
|
+
client_id: uniqueId
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
if (!resp.ok) {
|
|
236
|
+
throw new MesyncAuthError(`Token exchange failed: ${await resp.text()}`);
|
|
237
|
+
}
|
|
238
|
+
const tokens = await resp.json();
|
|
239
|
+
tokenStorage.setTokens(tokens.access_token, tokens.refresh_token);
|
|
240
|
+
}
|
|
241
|
+
var inFlightCallback = null;
|
|
242
|
+
function handleCallback() {
|
|
243
|
+
const code = new URLSearchParams(window.location.search).get("code");
|
|
244
|
+
if (code && inFlightCallback?.code === code) {
|
|
245
|
+
return inFlightCallback.promise;
|
|
246
|
+
}
|
|
247
|
+
const promise = (async () => {
|
|
248
|
+
try {
|
|
249
|
+
await exchangeCodeForTokens();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (isPopup()) {
|
|
252
|
+
const message = err instanceof Error ? err.message : "Could not connect to mesync.";
|
|
253
|
+
notifyOpenerAndClose({ type: "mesync-error", message });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
if (isPopup()) {
|
|
259
|
+
notifyOpenerAndClose({ type: "mesync-connected" });
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
if (code) inFlightCallback = { code, promise };
|
|
263
|
+
return promise;
|
|
264
|
+
}
|
|
265
|
+
var inFlightRefresh = null;
|
|
266
|
+
async function refreshAccessToken() {
|
|
267
|
+
if (inFlightRefresh) return inFlightRefresh;
|
|
268
|
+
const { mesyncUrl } = requireConfig();
|
|
269
|
+
const refreshToken = tokenStorage.getRefreshToken();
|
|
270
|
+
if (!refreshToken) throw new MesyncAuthError("Not logged in");
|
|
271
|
+
inFlightRefresh = (async () => {
|
|
272
|
+
const resp = await fetch(`${mesyncUrl}/api/oauth/token`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "Content-Type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ grant_type: "refresh_token", refresh_token: refreshToken })
|
|
276
|
+
});
|
|
277
|
+
if (!resp.ok) {
|
|
278
|
+
tokenStorage.clear();
|
|
279
|
+
throw new MesyncAuthError("Session expired \u2014 call mesync.login(...) again");
|
|
280
|
+
}
|
|
281
|
+
const tokens = await resp.json();
|
|
282
|
+
tokenStorage.setTokens(tokens.access_token, tokens.refresh_token);
|
|
283
|
+
return tokens.access_token;
|
|
284
|
+
})();
|
|
285
|
+
try {
|
|
286
|
+
return await inFlightRefresh;
|
|
287
|
+
} finally {
|
|
288
|
+
inFlightRefresh = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function authedFetch(path, init = {}) {
|
|
292
|
+
const { mesyncUrl } = requireConfig();
|
|
293
|
+
let accessToken = tokenStorage.getAccessToken();
|
|
294
|
+
if (!accessToken) throw new MesyncAuthError("Not logged in \u2014 call mesync.login(...) first");
|
|
295
|
+
const doFetch = (token) => fetch(`${mesyncUrl}${path}`, {
|
|
296
|
+
...init,
|
|
297
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, ...init.headers }
|
|
298
|
+
});
|
|
299
|
+
let resp = await doFetch(accessToken);
|
|
300
|
+
if (resp.status === 401) {
|
|
301
|
+
accessToken = await refreshAccessToken();
|
|
302
|
+
resp = await doFetch(accessToken);
|
|
303
|
+
}
|
|
304
|
+
return resp;
|
|
305
|
+
}
|
|
306
|
+
async function asJson(resp) {
|
|
307
|
+
if (!resp.ok) throw new Error(`mesync request failed (${resp.status}): ${await resp.text()}`);
|
|
308
|
+
if (resp.status === 204) return void 0;
|
|
309
|
+
return resp.json();
|
|
310
|
+
}
|
|
311
|
+
function table(namespace, name) {
|
|
312
|
+
const path = `/api/store/${namespace}/${name}`;
|
|
313
|
+
return {
|
|
314
|
+
async list(page = 1, pageSize = 20) {
|
|
315
|
+
return asJson(await authedFetch(`${path}?page=${page}&page_size=${pageSize}`));
|
|
316
|
+
},
|
|
317
|
+
async get(rowId) {
|
|
318
|
+
return asJson(await authedFetch(`${path}/${rowId}`));
|
|
319
|
+
},
|
|
320
|
+
async insert(data) {
|
|
321
|
+
return asJson(await authedFetch(path, { method: "POST", body: JSON.stringify(data) }));
|
|
322
|
+
},
|
|
323
|
+
async update(rowId, data) {
|
|
324
|
+
return asJson(await authedFetch(`${path}/${rowId}`, { method: "PATCH", body: JSON.stringify(data) }));
|
|
325
|
+
},
|
|
326
|
+
async remove(rowId) {
|
|
327
|
+
return asJson(await authedFetch(`${path}/${rowId}`, { method: "DELETE" }));
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function isLoggedIn() {
|
|
332
|
+
return tokenStorage.getAccessToken() !== null;
|
|
333
|
+
}
|
|
334
|
+
function logout() {
|
|
335
|
+
tokenStorage.clear();
|
|
336
|
+
}
|
|
337
|
+
async function registerManifest(manifest, token, options = {}) {
|
|
338
|
+
const { mesyncUrl } = requireConfig();
|
|
339
|
+
const resp = await fetch(`${mesyncUrl}/api/apps/manifest`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
unique_id: manifest.uniqueId,
|
|
344
|
+
name: manifest.name,
|
|
345
|
+
description: manifest.description,
|
|
346
|
+
redirect_uris: manifest.redirectUris,
|
|
347
|
+
tables: manifest.tables,
|
|
348
|
+
force: options.force ?? false
|
|
349
|
+
})
|
|
350
|
+
});
|
|
351
|
+
if (resp.status === 409) {
|
|
352
|
+
const body = await resp.json();
|
|
353
|
+
const detail = body.detail ?? {};
|
|
354
|
+
if (detail.error === "diverged") {
|
|
355
|
+
throw new MesyncManifestDivergedError(detail.message ?? "This manifest doesn't match what's registered.");
|
|
356
|
+
}
|
|
357
|
+
throw new MesyncUniqueIdConflictError(
|
|
358
|
+
manifest.uniqueId,
|
|
359
|
+
detail.suggested_unique_id ?? null,
|
|
360
|
+
detail.message ?? `unique_id ${manifest.uniqueId} is already taken`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (!resp.ok) {
|
|
364
|
+
throw new Error(`registerManifest failed: ${resp.status} ${await resp.text()}`);
|
|
365
|
+
}
|
|
366
|
+
const result = await resp.json();
|
|
367
|
+
return { status: result.status, migrationsApplied: result.migrations_applied ?? {} };
|
|
368
|
+
}
|
|
369
|
+
var mesync = {
|
|
370
|
+
configure,
|
|
371
|
+
login,
|
|
372
|
+
loginPopup,
|
|
373
|
+
handleCallback,
|
|
374
|
+
table,
|
|
375
|
+
isLoggedIn,
|
|
376
|
+
logout,
|
|
377
|
+
registerManifest
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export {
|
|
381
|
+
MesyncAuthError,
|
|
382
|
+
MesyncUniqueIdConflictError,
|
|
383
|
+
MesyncManifestDivergedError,
|
|
384
|
+
mesync
|
|
385
|
+
};
|
|
386
|
+
//# sourceMappingURL=chunk-YPMJ6DJU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["function base64UrlEncode(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nexport function randomUrlSafeString(byteLength = 32): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\nexport async function pkceChallengeFromVerifier(verifier: string): Promise<string> {\n const data = new TextEncoder().encode(verifier);\n const digest = await crypto.subtle.digest(\"SHA-256\", data);\n return base64UrlEncode(new Uint8Array(digest));\n}\n","const ACCESS_TOKEN_KEY = \"mesync_access_token\";\nconst REFRESH_TOKEN_KEY = \"mesync_refresh_token\";\nconst PKCE_VERIFIER_KEY = \"mesync_pkce_verifier\";\nconst OAUTH_STATE_KEY = \"mesync_oauth_state\";\n\nexport const tokenStorage = {\n getAccessToken: (): string | null => localStorage.getItem(ACCESS_TOKEN_KEY),\n getRefreshToken: (): string | null => localStorage.getItem(REFRESH_TOKEN_KEY),\n setTokens(accessToken: string, refreshToken: string): void {\n localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);\n localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);\n },\n clear(): void {\n localStorage.removeItem(ACCESS_TOKEN_KEY);\n localStorage.removeItem(REFRESH_TOKEN_KEY);\n },\n};\n\n// Deliberately localStorage, not sessionStorage: in popup mode, the popup\n// navigates to a different origin (mesyncUrl) for consent and back, and an\n// auxiliary window only shares its opener's sessionStorage while it stays\n// same-origin — that round trip can lose it. localStorage is keyed purely by\n// origin with no such browsing-context restriction, so it survives reliably\n// in both the popup and plain-redirect flows. consume() deletes it right\n// after reading, so it's still short-lived in practice.\nexport const pkceStorage = {\n save(verifier: string, state: string): void {\n localStorage.setItem(PKCE_VERIFIER_KEY, verifier);\n localStorage.setItem(OAUTH_STATE_KEY, state);\n },\n consume(): { verifier: string; state: string } | null {\n const verifier = localStorage.getItem(PKCE_VERIFIER_KEY);\n const state = localStorage.getItem(OAUTH_STATE_KEY);\n localStorage.removeItem(PKCE_VERIFIER_KEY);\n localStorage.removeItem(OAUTH_STATE_KEY);\n if (!verifier || !state) return null;\n return { verifier, state };\n },\n};\n","import { pkceChallengeFromVerifier, randomUrlSafeString } from \"./pkce\";\nimport { pkceStorage, tokenStorage } from \"./storage\";\nimport type {\n CatalogTable,\n MesyncConfig,\n MesyncManifest,\n MesyncManifestResult,\n Row,\n RowList,\n ScopeRequest,\n} from \"./types\";\n\nclass MesyncAuthError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MesyncAuthError\";\n }\n}\n\nclass MesyncUniqueIdConflictError extends Error {\n constructor(\n public readonly uniqueId: string,\n public readonly suggestedUniqueId: string | null,\n message: string\n ) {\n super(message);\n this.name = \"MesyncUniqueIdConflictError\";\n }\n}\n\nclass MesyncManifestDivergedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MesyncManifestDivergedError\";\n }\n}\n\nlet config: MesyncConfig | null = null;\n\nfunction requireConfig(): MesyncConfig {\n if (!config) {\n throw new Error(\"mesync.configure(...) must be called before using the SDK\");\n }\n return config;\n}\n\nfunction configure(newConfig: MesyncConfig): void {\n config = newConfig;\n}\n\nasync function resolveTableDefId(namespace: string, table: string): Promise<number> {\n const { mesyncUrl } = requireConfig();\n const resp = await fetch(`${mesyncUrl}/api/catalog/apps/${namespace}/tables`);\n if (resp.status === 404) {\n throw new Error(\n `App '${namespace}' isn't registered in the mesync catalog yet, so '${namespace}.${table}' can't be resolved. ` +\n `It needs to complete its own connect flow (with its manifest) at least once before other apps can request cross-app access to its tables.`\n );\n }\n if (!resp.ok) {\n throw new Error(\n `Could not look up table '${namespace}.${table}' in the mesync catalog (HTTP ${resp.status})`\n );\n }\n const tables: CatalogTable[] = await resp.json();\n const match = tables.find(t => t.name === table);\n if (!match) {\n throw new Error(`Table '${table}' not found under namespace '${namespace}'`);\n }\n return match.table_def_id;\n}\n\nfunction isOwnScope(s: ScopeRequest, uniqueId: string | undefined): boolean {\n return uniqueId !== undefined && s.namespace === uniqueId;\n}\n\n/**\n * Builds the mesync consent-screen URL. Scopes on the connecting app's own\n * `manifest.uniqueId` are sent as name-based `own_scope` entries — deferred,\n * server-side resolution, since that table may not exist yet (a brand-new\n * app) or be mid-migration. Every other scope (\"cross\", reading another\n * app's already-existing table) is still resolved here via the catalog, as\n * that table must already exist for a read to make sense.\n *\n * `manifest`, if given, rides along as an extra query param — mesync's own\n * hosted authorize page (not this SDK) decides whether it needs registering,\n * needs a schema update, or is already in sync, and renders the appropriate\n * screen. This SDK never makes that decision itself.\n */\nasync function buildAuthorizeUrl(scopes: ScopeRequest[], manifest?: MesyncManifest): Promise<string> {\n const { mesyncUrl, uniqueId, redirectUri } = requireConfig();\n if (scopes.length === 0) {\n throw new Error(\"login() requires at least one scope\");\n }\n\n const ownScopes = scopes.filter(s => isOwnScope(s, manifest?.uniqueId));\n const crossScopes = scopes.filter(s => !isOwnScope(s, manifest?.uniqueId));\n\n const crossEntries = await Promise.all(\n crossScopes.map(async s => `${await resolveTableDefId(s.namespace, s.table)}:${s.permission}`)\n );\n\n const verifier = randomUrlSafeString();\n const challenge = await pkceChallengeFromVerifier(verifier);\n const state = randomUrlSafeString(16);\n pkceStorage.save(verifier, state);\n\n const query = new URLSearchParams({\n // Wire-protocol param name per OAuth2 convention — its value is the app's uniqueId.\n client_id: uniqueId,\n redirect_uri: redirectUri,\n code_challenge: challenge,\n code_challenge_method: \"S256\",\n state,\n // The app's own origin, so the platform can link back to it once connected.\n app_url: window.location.origin,\n });\n if (crossEntries.length) {\n query.set(\"scope\", crossEntries.join(\",\"));\n if (crossScopes.some(s => s.reason)) {\n query.set(\"reasons\", crossScopes.map(s => encodeURIComponent(s.reason ?? \"\")).join(\",\"));\n }\n }\n if (ownScopes.length) {\n query.set(\"own_scope\", ownScopes.map(s => `${s.table}:${s.permission}`).join(\",\"));\n if (ownScopes.some(s => s.reason)) {\n query.set(\"own_reasons\", ownScopes.map(s => encodeURIComponent(s.reason ?? \"\")).join(\",\"));\n }\n }\n if (manifest) {\n query.set(\n \"manifest\",\n JSON.stringify({\n unique_id: manifest.uniqueId,\n name: manifest.name,\n description: manifest.description,\n redirect_uris: manifest.redirectUris,\n tables: manifest.tables,\n })\n );\n }\n return `${mesyncUrl}/oauth/authorize?${query.toString()}`;\n}\n\ninterface LoginOptions {\n /** If given, mesync's own hosted authorize page decides whether\n * `manifest.uniqueId` needs registering, needs a schema update, or is\n * already in sync, and shows the right screen — nothing about that\n * decision happens here. */\n manifest?: MesyncManifest;\n}\n\n/**\n * Redirect the browser to the mesync consent screen requesting the given scopes.\n * Call this from your \"Connect with mesync\" button's onClick handler.\n */\nasync function login(scopes: ScopeRequest[], options: LoginOptions = {}): Promise<void> {\n window.location.href = await buildAuthorizeUrl(scopes, options.manifest);\n}\n\n/**\n * Same as login(), but drives the consent screen in a popup window instead of\n * navigating the current page away. Resolves once the popup completes (and\n * closes itself) via handleCallback()'s popup-completion handshake; rejects if\n * the popup is blocked, fails, or is closed before completing.\n *\n * Must be called directly from a user gesture (e.g. a click handler) with no\n * `await` ahead of it — the popup is opened synchronously first, then\n * navigated once the URL is built, so browsers don't treat it as blocked.\n */\nfunction loginPopup(scopes: ScopeRequest[], options: LoginOptions = {}): Promise<void> {\n const popup = window.open(\"\", \"mesync-connect\", \"width=480,height=640,menubar=no,toolbar=no,status=no\");\n if (!popup) {\n return Promise.reject(\n new MesyncAuthError(\"Popup blocked — allow popups for this site, or use mesync.login() instead.\")\n );\n }\n\n return new Promise((resolve, reject) => {\n let settled = false;\n\n function cleanup() {\n window.removeEventListener(\"message\", handleMessage);\n clearInterval(closePoll);\n }\n\n function handleMessage(e: MessageEvent) {\n if (e.source !== popup || e.origin !== window.location.origin) return;\n settled = true;\n cleanup();\n if (e.data?.type === \"mesync-connected\") {\n resolve();\n } else if (e.data?.type === \"mesync-error\") {\n reject(new MesyncAuthError(e.data.message ?? \"Could not connect to mesync.\"));\n }\n }\n\n window.addEventListener(\"message\", handleMessage);\n const closePoll = window.setInterval(() => {\n if (popup.closed && !settled) {\n settled = true;\n cleanup();\n reject(new MesyncAuthError(\"Connection window was closed before completing.\"));\n }\n }, 500);\n\n buildAuthorizeUrl(scopes, options.manifest)\n .then(url => {\n popup.location.href = url;\n })\n .catch(err => {\n if (settled) return;\n settled = true;\n cleanup();\n popup.close();\n reject(err instanceof Error ? err : new MesyncAuthError(\"Could not connect to mesync.\"));\n });\n });\n}\n\nfunction isPopup(): boolean {\n return !!window.opener && window.opener !== window;\n}\n\nfunction notifyOpenerAndClose(payload: { type: \"mesync-connected\" } | { type: \"mesync-error\"; message: string }) {\n window.opener.postMessage(payload, window.location.origin);\n window.close();\n}\n\nasync function exchangeCodeForTokens(): Promise<void> {\n const { mesyncUrl, uniqueId, redirectUri } = requireConfig();\n const params = new URLSearchParams(window.location.search);\n\n const errorParam = params.get(\"error\");\n if (errorParam) {\n throw new MesyncAuthError(`mesync authorization was denied: ${errorParam}`);\n }\n\n const code = params.get(\"code\");\n const returnedState = params.get(\"state\");\n if (!code || !returnedState) {\n throw new MesyncAuthError(\"Missing code/state in callback URL\");\n }\n\n const saved = pkceStorage.consume();\n if (!saved || saved.state !== returnedState) {\n throw new MesyncAuthError(\"OAuth state mismatch — possible CSRF, aborting\");\n }\n\n const resp = await fetch(`${mesyncUrl}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n code_verifier: saved.verifier,\n client_id: uniqueId,\n }),\n });\n if (!resp.ok) {\n throw new MesyncAuthError(`Token exchange failed: ${await resp.text()}`);\n }\n const tokens = await resp.json();\n tokenStorage.setTokens(tokens.access_token, tokens.refresh_token);\n}\n\n// Keyed by the `code` query param so a duplicate handleCallback() call for the\n// same callback (e.g. React 18 StrictMode's dev-only mount/unmount/remount of\n// <mesync-callback>) awaits the original exchange instead of re-consuming the\n// already-spent PKCE verifier/state and failing with a false CSRF error.\nlet inFlightCallback: { code: string; promise: Promise<void> } | null = null;\n\n/**\n * Call this on your redirect_uri page after the user approves (or denies) the\n * consent screen. Exchanges the authorization code for tokens and stores them.\n *\n * If this page is running inside a popup opened by loginPopup(), it instead\n * reports success/failure back to the opener via postMessage and closes\n * itself — the opener's loginPopup() promise settles from that message.\n */\nfunction handleCallback(): Promise<void> {\n const code = new URLSearchParams(window.location.search).get(\"code\");\n if (code && inFlightCallback?.code === code) {\n return inFlightCallback.promise;\n }\n\n const promise = (async () => {\n try {\n await exchangeCodeForTokens();\n } catch (err) {\n if (isPopup()) {\n const message = err instanceof Error ? err.message : \"Could not connect to mesync.\";\n notifyOpenerAndClose({ type: \"mesync-error\", message });\n return;\n }\n throw err;\n }\n if (isPopup()) {\n notifyOpenerAndClose({ type: \"mesync-connected\" });\n }\n })();\n\n if (code) inFlightCallback = { code, promise };\n return promise;\n}\n\n// Refresh tokens rotate on every use — a second concurrent call (e.g. two\n// authedFetch calls 401ing at once) would race to redeem the same refresh\n// token, and the loser would be treated as a stolen/reused token by the\n// server. Dedup concurrent calls into a single in-flight exchange so only\n// one redemption ever happens at a time, mirroring inFlightCallback above.\nlet inFlightRefresh: Promise<string> | null = null;\n\nasync function refreshAccessToken(): Promise<string> {\n if (inFlightRefresh) return inFlightRefresh;\n\n const { mesyncUrl } = requireConfig();\n const refreshToken = tokenStorage.getRefreshToken();\n if (!refreshToken) throw new MesyncAuthError(\"Not logged in\");\n\n inFlightRefresh = (async () => {\n const resp = await fetch(`${mesyncUrl}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ grant_type: \"refresh_token\", refresh_token: refreshToken }),\n });\n if (!resp.ok) {\n tokenStorage.clear();\n throw new MesyncAuthError(\"Session expired — call mesync.login(...) again\");\n }\n const tokens = await resp.json();\n tokenStorage.setTokens(tokens.access_token, tokens.refresh_token);\n return tokens.access_token;\n })();\n\n try {\n return await inFlightRefresh;\n } finally {\n inFlightRefresh = null;\n }\n}\n\nasync function authedFetch(path: string, init: RequestInit = {}): Promise<Response> {\n const { mesyncUrl } = requireConfig();\n let accessToken = tokenStorage.getAccessToken();\n if (!accessToken) throw new MesyncAuthError(\"Not logged in — call mesync.login(...) first\");\n\n const doFetch = (token: string) =>\n fetch(`${mesyncUrl}${path}`, {\n ...init,\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${token}`, ...init.headers },\n });\n\n let resp = await doFetch(accessToken);\n if (resp.status === 401) {\n accessToken = await refreshAccessToken();\n resp = await doFetch(accessToken);\n }\n return resp;\n}\n\nasync function asJson<T>(resp: Response): Promise<T> {\n if (!resp.ok) throw new Error(`mesync request failed (${resp.status}): ${await resp.text()}`);\n if (resp.status === 204) return undefined as T;\n return resp.json() as Promise<T>;\n}\n\n/** A handle for reading/writing a single mesync-hosted table. */\nfunction table(namespace: string, name: string) {\n const path = `/api/store/${namespace}/${name}`;\n return {\n async list(page = 1, pageSize = 20): Promise<RowList> {\n return asJson<RowList>(await authedFetch(`${path}?page=${page}&page_size=${pageSize}`));\n },\n async get(rowId: number): Promise<Row> {\n return asJson<Row>(await authedFetch(`${path}/${rowId}`));\n },\n async insert(data: Record<string, unknown>): Promise<Row> {\n return asJson<Row>(await authedFetch(path, { method: \"POST\", body: JSON.stringify(data) }));\n },\n async update(rowId: number, data: Record<string, unknown>): Promise<Row> {\n return asJson<Row>(await authedFetch(`${path}/${rowId}`, { method: \"PATCH\", body: JSON.stringify(data) }));\n },\n async remove(rowId: number): Promise<void> {\n return asJson<void>(await authedFetch(`${path}/${rowId}`, { method: \"DELETE\" }));\n },\n };\n}\n\nfunction isLoggedIn(): boolean {\n return tokenStorage.getAccessToken() !== null;\n}\n\nfunction logout(): void {\n tokenStorage.clear();\n}\n\n/**\n * Direct, scriptable registration call — for CI/setup scripts, not the\n * interactive connect flow (that's handled entirely by mesync's own hosted\n * authorize page now; see `login`/`loginPopup`'s `manifest` option). Takes\n * a bearer token the caller already obtained however they like (e.g. their\n * own `POST /api/auth/login` call) — this function does no credential\n * management of its own.\n *\n * Safe to call any number of times: a new `uniqueId` creates the app; the\n * same owner resubmitting diffs tables and applies only safe schema changes\n * (new tables/columns, safe retypes); a `uniqueId` already owned by someone\n * else throws MesyncUniqueIdConflictError; a manifest that shares no tables\n * at all with what's already registered under your own `uniqueId` throws\n * MesyncManifestDivergedError unless `force: true` is passed.\n */\nasync function registerManifest(\n manifest: MesyncManifest,\n token: string,\n options: { force?: boolean } = {}\n): Promise<MesyncManifestResult> {\n const { mesyncUrl } = requireConfig();\n\n const resp = await fetch(`${mesyncUrl}/api/apps/manifest`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${token}` },\n body: JSON.stringify({\n unique_id: manifest.uniqueId,\n name: manifest.name,\n description: manifest.description,\n redirect_uris: manifest.redirectUris,\n tables: manifest.tables,\n force: options.force ?? false,\n }),\n });\n\n if (resp.status === 409) {\n const body = await resp.json();\n const detail = body.detail ?? {};\n if (detail.error === \"diverged\") {\n throw new MesyncManifestDivergedError(detail.message ?? \"This manifest doesn't match what's registered.\");\n }\n throw new MesyncUniqueIdConflictError(\n manifest.uniqueId,\n detail.suggested_unique_id ?? null,\n detail.message ?? `unique_id ${manifest.uniqueId} is already taken`\n );\n }\n if (!resp.ok) {\n throw new Error(`registerManifest failed: ${resp.status} ${await resp.text()}`);\n }\n\n const result = await resp.json();\n return { status: result.status, migrationsApplied: result.migrations_applied ?? {} };\n}\n\nexport const mesync = {\n configure,\n login,\n loginPopup,\n handleCallback,\n table,\n isLoggedIn,\n logout,\n registerManifest,\n};\n\nexport { MesyncAuthError, MesyncUniqueIdConflictError, MesyncManifestDivergedError };\nexport type { MesyncConfig, ScopeRequest, Row, RowList };\n"],"mappings":"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","names":["table"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prebuilt "Connect with mesync" button. Framework-agnostic (plain custom
|
|
3
|
+
* element) so it can drop into React/Vue/Svelte/etc without a framework-
|
|
4
|
+
* specific SDK build. Usage:
|
|
5
|
+
*
|
|
6
|
+
* <mesync-connect scopes='[{"namespace":"move","table":"logs","permission":"write"}]'></mesync-connect>
|
|
7
|
+
*
|
|
8
|
+
* By default (mode="redirect", or omitted) this navigates the whole page to
|
|
9
|
+
* the consent screen, so completion is observed on the redirect_uri page via
|
|
10
|
+
* <mesync-callback> instead of here. Set mode="popup" to instead drive the
|
|
11
|
+
* consent screen in a popup window and get a "mesync-connected" event on
|
|
12
|
+
* this element once it completes, with no page navigation.
|
|
13
|
+
*
|
|
14
|
+
* Pass a `manifest` attribute (JSON, same shape as defineManifest()'s
|
|
15
|
+
* argument) to fold app registration into this same button — it's just
|
|
16
|
+
* forwarded along to mesync's own hosted consent screen, which checks
|
|
17
|
+
* whether `manifest.uniqueId` needs registering, needs a schema update, or
|
|
18
|
+
* is already in sync, and shows the right screen there (including, on the
|
|
19
|
+
* very first-ever connect, a real login page hosted on mesync's own origin —
|
|
20
|
+
* never a form rendered by this element or by your app). This element makes
|
|
21
|
+
* no registration decisions itself.
|
|
22
|
+
*
|
|
23
|
+
* Renders `label`/`connected-label` as plain text by default, but any light-DOM
|
|
24
|
+
* children take over instead — e.g. drop in an icon or custom markup:
|
|
25
|
+
*
|
|
26
|
+
* <mesync-connect scopes='...'><img src="/icon.svg" alt="Connect"></mesync-connect>
|
|
27
|
+
*
|
|
28
|
+
* The host toggles `connecting`/`connected` boolean attributes so custom
|
|
29
|
+
* content can react via CSS (e.g. `mesync-connect[connecting] img { opacity: .5 }`).
|
|
30
|
+
* Styling hooks: `::part(button)`, `::part(error)`, and the CSS custom
|
|
31
|
+
* properties --mesync-bg, --mesync-color, --mesync-radius, --mesync-error-color.
|
|
32
|
+
*
|
|
33
|
+
* Emits "mesync-error" if the connection fails (scope resolution before the
|
|
34
|
+
* redirect, a blocked/closed popup, or a rejected popup flow).
|
|
35
|
+
*/
|
|
36
|
+
declare class MesyncConnectElement extends HTMLElement {
|
|
37
|
+
#private;
|
|
38
|
+
static get observedAttributes(): string[];
|
|
39
|
+
constructor();
|
|
40
|
+
connectedCallback(): void;
|
|
41
|
+
attributeChangedCallback(): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Drop-in replacement for the redirect_uri page's callback handler. Calls
|
|
46
|
+
* mesync.handleCallback() on mount, then either redirects (default: back to
|
|
47
|
+
* "/") or emits "mesync-connected" for the host app to handle itself.
|
|
48
|
+
*
|
|
49
|
+
* <mesync-callback redirect-to="./"></mesync-callback>
|
|
50
|
+
*
|
|
51
|
+
* Set redirect-to="" (empty) to suppress the redirect and just listen for
|
|
52
|
+
* "mesync-connected" / "mesync-error" instead.
|
|
53
|
+
*/
|
|
54
|
+
declare class MesyncCallbackElement extends HTMLElement {
|
|
55
|
+
#private;
|
|
56
|
+
constructor();
|
|
57
|
+
connectedCallback(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { MesyncCallbackElement, MesyncConnectElement };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { S as ScopeRequest, M as MesyncManifest } from '../types-CRSr6Kwh.js';
|
|
3
|
+
|
|
4
|
+
interface MesyncConnectProps {
|
|
5
|
+
scopes: ScopeRequest[];
|
|
6
|
+
label?: string;
|
|
7
|
+
connectedLabel?: string;
|
|
8
|
+
/** "redirect" (default) navigates the page to the consent screen; "popup" drives it in a popup window and fires onConnected in place, with no navigation. */
|
|
9
|
+
mode?: "redirect" | "popup";
|
|
10
|
+
/** If given, this button also folds in app registration — mesync's own
|
|
11
|
+
* hosted consent screen checks whether `manifest.uniqueId` needs
|
|
12
|
+
* registering, needs a schema update, or is already in sync, and shows
|
|
13
|
+
* the right screen there. Nothing about that decision happens here. */
|
|
14
|
+
manifest?: MesyncManifest;
|
|
15
|
+
/** Only fires in mode="popup" — the redirect flow completes on the redirect_uri page instead, via <MesyncCallback>. */
|
|
16
|
+
onConnected?: () => void;
|
|
17
|
+
/** Fires if resolving scopes / redirecting to the consent screen fails (or, in popup mode, if the popup is blocked, closed, or fails). */
|
|
18
|
+
onError?: (message: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/** React wrapper around the framework-agnostic <mesync-connect> custom element. */
|
|
21
|
+
declare function MesyncConnect({ scopes, label, connectedLabel, mode, manifest, onConnected, onError, }: MesyncConnectProps): ReactElement;
|
|
22
|
+
interface MesyncCallbackProps {
|
|
23
|
+
/** Where to send the browser after a successful connection. Defaults to "./"; pass "" to disable and rely on onConnected instead. */
|
|
24
|
+
redirectTo?: string;
|
|
25
|
+
onConnected?: () => void;
|
|
26
|
+
onError?: (message: string) => void;
|
|
27
|
+
}
|
|
28
|
+
/** React wrapper around the framework-agnostic <mesync-callback> custom element. */
|
|
29
|
+
declare function MesyncCallback({ redirectTo, onConnected, onError }: MesyncCallbackProps): ReactElement;
|
|
30
|
+
interface UseMesyncConnectOptions {
|
|
31
|
+
/** "redirect" (default) navigates the page away; "popup" drives the consent screen in a popup and resolves in place. */
|
|
32
|
+
mode?: "redirect" | "popup";
|
|
33
|
+
/** Same registration-folding behavior as <MesyncConnect>'s `manifest` prop — see there for details. */
|
|
34
|
+
manifest?: MesyncManifest;
|
|
35
|
+
}
|
|
36
|
+
interface UseMesyncConnectResult {
|
|
37
|
+
/** Call from any onClick — an icon button, a menu item, whatever markup you want. */
|
|
38
|
+
connect: () => void;
|
|
39
|
+
connecting: boolean;
|
|
40
|
+
/** Message from the last failed attempt, if any. Cleared on the next connect() call. */
|
|
41
|
+
error: string | null;
|
|
42
|
+
isLoggedIn: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Headless alternative to <MesyncConnect> for when you don't want the
|
|
46
|
+
* built-in button markup — render an icon, a custom component, anything —
|
|
47
|
+
* and wire it up yourself:
|
|
48
|
+
*
|
|
49
|
+
* const { connect, connecting, isLoggedIn } = useMesyncConnect(scopes);
|
|
50
|
+
* <IconButton icon={<PlugIcon />} disabled={connecting || isLoggedIn} onClick={connect} />
|
|
51
|
+
*/
|
|
52
|
+
declare function useMesyncConnect(scopes: ScopeRequest[], { mode, manifest }?: UseMesyncConnectOptions): UseMesyncConnectResult;
|
|
53
|
+
|
|
54
|
+
export { MesyncCallback, type MesyncCallbackProps, MesyncConnect, type MesyncConnectProps, type UseMesyncConnectOptions, type UseMesyncConnectResult, useMesyncConnect };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "../chunk-E3FGRAPR.js";
|
|
2
|
+
import {
|
|
3
|
+
mesync
|
|
4
|
+
} from "../chunk-YPMJ6DJU.js";
|
|
5
|
+
|
|
6
|
+
// src/elements/react.tsx
|
|
7
|
+
import { createElement, useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
function useErrorEvent(ref, onError) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const el = ref.current;
|
|
11
|
+
if (!el || !onError) return;
|
|
12
|
+
const listener = (e) => onError(e.detail);
|
|
13
|
+
el.addEventListener("mesync-error", listener);
|
|
14
|
+
return () => el.removeEventListener("mesync-error", listener);
|
|
15
|
+
}, [onError]);
|
|
16
|
+
}
|
|
17
|
+
function useConnectedEvent(ref, onConnected) {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const el = ref.current;
|
|
20
|
+
if (!el || !onConnected) return;
|
|
21
|
+
el.addEventListener("mesync-connected", onConnected);
|
|
22
|
+
return () => el.removeEventListener("mesync-connected", onConnected);
|
|
23
|
+
}, [onConnected]);
|
|
24
|
+
}
|
|
25
|
+
function MesyncConnect({
|
|
26
|
+
scopes,
|
|
27
|
+
label,
|
|
28
|
+
connectedLabel,
|
|
29
|
+
mode,
|
|
30
|
+
manifest,
|
|
31
|
+
onConnected,
|
|
32
|
+
onError
|
|
33
|
+
}) {
|
|
34
|
+
const ref = useRef(null);
|
|
35
|
+
useConnectedEvent(ref, onConnected);
|
|
36
|
+
useErrorEvent(ref, onError);
|
|
37
|
+
return createElement("mesync-connect", {
|
|
38
|
+
ref,
|
|
39
|
+
scopes: JSON.stringify(scopes),
|
|
40
|
+
label,
|
|
41
|
+
"connected-label": connectedLabel,
|
|
42
|
+
mode,
|
|
43
|
+
manifest: manifest ? JSON.stringify(manifest) : void 0
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function MesyncCallback({ redirectTo, onConnected, onError }) {
|
|
47
|
+
const ref = useRef(null);
|
|
48
|
+
useConnectedEvent(ref, onConnected);
|
|
49
|
+
useErrorEvent(ref, onError);
|
|
50
|
+
return createElement("mesync-callback", { ref, "redirect-to": redirectTo });
|
|
51
|
+
}
|
|
52
|
+
function useMesyncConnect(scopes, { mode = "redirect", manifest } = {}) {
|
|
53
|
+
const [connecting, setConnecting] = useState(false);
|
|
54
|
+
const [error, setError] = useState(null);
|
|
55
|
+
const [isLoggedIn, setIsLoggedIn] = useState(() => mesync.isLoggedIn());
|
|
56
|
+
const connect = useCallback(() => {
|
|
57
|
+
setError(null);
|
|
58
|
+
setConnecting(true);
|
|
59
|
+
const options = manifest ? { manifest } : void 0;
|
|
60
|
+
const promise = mode === "popup" ? mesync.loginPopup(scopes, options) : mesync.login(scopes, options);
|
|
61
|
+
void promise.then(() => setIsLoggedIn(true)).catch((err) => {
|
|
62
|
+
setError(err instanceof Error ? err.message : "Could not connect to mesync.");
|
|
63
|
+
}).finally(() => setConnecting(false));
|
|
64
|
+
}, [scopes, mode, manifest]);
|
|
65
|
+
return { connect, connecting, error, isLoggedIn };
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
MesyncCallback,
|
|
69
|
+
MesyncConnect,
|
|
70
|
+
useMesyncConnect
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/elements/react.tsx"],"sourcesContent":["import { createElement, useCallback, useEffect, useRef, useState, type ReactElement } from \"react\";\nimport \"./connect-element\";\nimport \"./callback-element\";\nimport { mesync } from \"../client\";\nimport type { MesyncManifest, ScopeRequest } from \"../types\";\n\nfunction useErrorEvent(ref: React.RefObject<HTMLElement | null>, onError?: (message: string) => void) {\n useEffect(() => {\n const el = ref.current;\n if (!el || !onError) return;\n const listener = (e: Event) => onError((e as CustomEvent<string>).detail);\n el.addEventListener(\"mesync-error\", listener);\n return () => el.removeEventListener(\"mesync-error\", listener);\n }, [onError]);\n}\n\nfunction useConnectedEvent(ref: React.RefObject<HTMLElement | null>, onConnected?: () => void) {\n useEffect(() => {\n const el = ref.current;\n if (!el || !onConnected) return;\n el.addEventListener(\"mesync-connected\", onConnected);\n return () => el.removeEventListener(\"mesync-connected\", onConnected);\n }, [onConnected]);\n}\n\nexport interface MesyncConnectProps {\n scopes: ScopeRequest[];\n label?: string;\n connectedLabel?: string;\n /** \"redirect\" (default) navigates the page to the consent screen; \"popup\" drives it in a popup window and fires onConnected in place, with no navigation. */\n mode?: \"redirect\" | \"popup\";\n /** If given, this button also folds in app registration — mesync's own\n * hosted consent screen checks whether `manifest.uniqueId` needs\n * registering, needs a schema update, or is already in sync, and shows\n * the right screen there. Nothing about that decision happens here. */\n manifest?: MesyncManifest;\n /** Only fires in mode=\"popup\" — the redirect flow completes on the redirect_uri page instead, via <MesyncCallback>. */\n onConnected?: () => void;\n /** Fires if resolving scopes / redirecting to the consent screen fails (or, in popup mode, if the popup is blocked, closed, or fails). */\n onError?: (message: string) => void;\n}\n\n/** React wrapper around the framework-agnostic <mesync-connect> custom element. */\nexport function MesyncConnect({\n scopes,\n label,\n connectedLabel,\n mode,\n manifest,\n onConnected,\n onError,\n}: MesyncConnectProps): ReactElement {\n const ref = useRef<HTMLElement | null>(null);\n useConnectedEvent(ref, onConnected);\n useErrorEvent(ref, onError);\n\n return createElement(\"mesync-connect\", {\n ref,\n scopes: JSON.stringify(scopes),\n label,\n \"connected-label\": connectedLabel,\n mode,\n manifest: manifest ? JSON.stringify(manifest) : undefined,\n });\n}\n\nexport interface MesyncCallbackProps {\n /** Where to send the browser after a successful connection. Defaults to \"./\"; pass \"\" to disable and rely on onConnected instead. */\n redirectTo?: string;\n onConnected?: () => void;\n onError?: (message: string) => void;\n}\n\n/** React wrapper around the framework-agnostic <mesync-callback> custom element. */\nexport function MesyncCallback({ redirectTo, onConnected, onError }: MesyncCallbackProps): ReactElement {\n const ref = useRef<HTMLElement | null>(null);\n useConnectedEvent(ref, onConnected);\n useErrorEvent(ref, onError);\n\n return createElement(\"mesync-callback\", { ref, \"redirect-to\": redirectTo });\n}\n\nexport interface UseMesyncConnectOptions {\n /** \"redirect\" (default) navigates the page away; \"popup\" drives the consent screen in a popup and resolves in place. */\n mode?: \"redirect\" | \"popup\";\n /** Same registration-folding behavior as <MesyncConnect>'s `manifest` prop — see there for details. */\n manifest?: MesyncManifest;\n}\n\nexport interface UseMesyncConnectResult {\n /** Call from any onClick — an icon button, a menu item, whatever markup you want. */\n connect: () => void;\n connecting: boolean;\n /** Message from the last failed attempt, if any. Cleared on the next connect() call. */\n error: string | null;\n isLoggedIn: boolean;\n}\n\n/**\n * Headless alternative to <MesyncConnect> for when you don't want the\n * built-in button markup — render an icon, a custom component, anything —\n * and wire it up yourself:\n *\n * const { connect, connecting, isLoggedIn } = useMesyncConnect(scopes);\n * <IconButton icon={<PlugIcon />} disabled={connecting || isLoggedIn} onClick={connect} />\n */\nexport function useMesyncConnect(\n scopes: ScopeRequest[],\n { mode = \"redirect\", manifest }: UseMesyncConnectOptions = {}\n): UseMesyncConnectResult {\n const [connecting, setConnecting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [isLoggedIn, setIsLoggedIn] = useState(() => mesync.isLoggedIn());\n\n const connect = useCallback(() => {\n setError(null);\n setConnecting(true);\n const options = manifest ? { manifest } : undefined;\n const promise = mode === \"popup\" ? mesync.loginPopup(scopes, options) : mesync.login(scopes, options);\n void promise\n .then(() => setIsLoggedIn(true))\n .catch((err: unknown) => {\n setError(err instanceof Error ? err.message : \"Could not connect to mesync.\");\n })\n .finally(() => setConnecting(false));\n // In \"redirect\" mode the browser navigates away before this ever settles.\n }, [scopes, mode, manifest]);\n\n return { connect, connecting, error, isLoggedIn };\n}\n"],"mappings":";;;;;;AAAA,SAAS,eAAe,aAAa,WAAW,QAAQ,gBAAmC;AAM3F,SAAS,cAAc,KAA0C,SAAqC;AACpG,YAAU,MAAM;AACd,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,CAAC,QAAS;AACrB,UAAM,WAAW,CAAC,MAAa,QAAS,EAA0B,MAAM;AACxE,OAAG,iBAAiB,gBAAgB,QAAQ;AAC5C,WAAO,MAAM,GAAG,oBAAoB,gBAAgB,QAAQ;AAAA,EAC9D,GAAG,CAAC,OAAO,CAAC;AACd;AAEA,SAAS,kBAAkB,KAA0C,aAA0B;AAC7F,YAAU,MAAM;AACd,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,CAAC,YAAa;AACzB,OAAG,iBAAiB,oBAAoB,WAAW;AACnD,WAAO,MAAM,GAAG,oBAAoB,oBAAoB,WAAW;AAAA,EACrE,GAAG,CAAC,WAAW,CAAC;AAClB;AAoBO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAqC;AACnC,QAAM,MAAM,OAA2B,IAAI;AAC3C,oBAAkB,KAAK,WAAW;AAClC,gBAAc,KAAK,OAAO;AAE1B,SAAO,cAAc,kBAAkB;AAAA,IACrC;AAAA,IACA,QAAQ,KAAK,UAAU,MAAM;AAAA,IAC7B;AAAA,IACA,mBAAmB;AAAA,IACnB;AAAA,IACA,UAAU,WAAW,KAAK,UAAU,QAAQ,IAAI;AAAA,EAClD,CAAC;AACH;AAUO,SAAS,eAAe,EAAE,YAAY,aAAa,QAAQ,GAAsC;AACtG,QAAM,MAAM,OAA2B,IAAI;AAC3C,oBAAkB,KAAK,WAAW;AAClC,gBAAc,KAAK,OAAO;AAE1B,SAAO,cAAc,mBAAmB,EAAE,KAAK,eAAe,WAAW,CAAC;AAC5E;AA0BO,SAAS,iBACd,QACA,EAAE,OAAO,YAAY,SAAS,IAA6B,CAAC,GACpC;AACxB,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,MAAM,OAAO,WAAW,CAAC;AAEtE,QAAM,UAAU,YAAY,MAAM;AAChC,aAAS,IAAI;AACb,kBAAc,IAAI;AAClB,UAAM,UAAU,WAAW,EAAE,SAAS,IAAI;AAC1C,UAAM,UAAU,SAAS,UAAU,OAAO,WAAW,QAAQ,OAAO,IAAI,OAAO,MAAM,QAAQ,OAAO;AACpG,SAAK,QACF,KAAK,MAAM,cAAc,IAAI,CAAC,EAC9B,MAAM,CAAC,QAAiB;AACvB,eAAS,eAAe,QAAQ,IAAI,UAAU,8BAA8B;AAAA,IAC9E,CAAC,EACA,QAAQ,MAAM,cAAc,KAAK,CAAC;AAAA,EAEvC,GAAG,CAAC,QAAQ,MAAM,QAAQ,CAAC;AAE3B,SAAO,EAAE,SAAS,YAAY,OAAO,WAAW;AAClD;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { a as MesyncConfig, S as ScopeRequest, M as MesyncManifest, R as RowList, b as Row, c as MesyncManifestResult } from './types-CRSr6Kwh.js';
|
|
2
|
+
export { d as MesyncColumnType, e as MesyncManifestColumn, f as MesyncManifestTable } from './types-CRSr6Kwh.js';
|
|
3
|
+
|
|
4
|
+
declare class MesyncAuthError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
declare class MesyncUniqueIdConflictError extends Error {
|
|
8
|
+
readonly uniqueId: string;
|
|
9
|
+
readonly suggestedUniqueId: string | null;
|
|
10
|
+
constructor(uniqueId: string, suggestedUniqueId: string | null, message: string);
|
|
11
|
+
}
|
|
12
|
+
declare class MesyncManifestDivergedError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
declare function configure(newConfig: MesyncConfig): void;
|
|
16
|
+
interface LoginOptions {
|
|
17
|
+
/** If given, mesync's own hosted authorize page decides whether
|
|
18
|
+
* `manifest.uniqueId` needs registering, needs a schema update, or is
|
|
19
|
+
* already in sync, and shows the right screen — nothing about that
|
|
20
|
+
* decision happens here. */
|
|
21
|
+
manifest?: MesyncManifest;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Redirect the browser to the mesync consent screen requesting the given scopes.
|
|
25
|
+
* Call this from your "Connect with mesync" button's onClick handler.
|
|
26
|
+
*/
|
|
27
|
+
declare function login(scopes: ScopeRequest[], options?: LoginOptions): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Same as login(), but drives the consent screen in a popup window instead of
|
|
30
|
+
* navigating the current page away. Resolves once the popup completes (and
|
|
31
|
+
* closes itself) via handleCallback()'s popup-completion handshake; rejects if
|
|
32
|
+
* the popup is blocked, fails, or is closed before completing.
|
|
33
|
+
*
|
|
34
|
+
* Must be called directly from a user gesture (e.g. a click handler) with no
|
|
35
|
+
* `await` ahead of it — the popup is opened synchronously first, then
|
|
36
|
+
* navigated once the URL is built, so browsers don't treat it as blocked.
|
|
37
|
+
*/
|
|
38
|
+
declare function loginPopup(scopes: ScopeRequest[], options?: LoginOptions): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Call this on your redirect_uri page after the user approves (or denies) the
|
|
41
|
+
* consent screen. Exchanges the authorization code for tokens and stores them.
|
|
42
|
+
*
|
|
43
|
+
* If this page is running inside a popup opened by loginPopup(), it instead
|
|
44
|
+
* reports success/failure back to the opener via postMessage and closes
|
|
45
|
+
* itself — the opener's loginPopup() promise settles from that message.
|
|
46
|
+
*/
|
|
47
|
+
declare function handleCallback(): Promise<void>;
|
|
48
|
+
/** A handle for reading/writing a single mesync-hosted table. */
|
|
49
|
+
declare function table(namespace: string, name: string): {
|
|
50
|
+
list(page?: number, pageSize?: number): Promise<RowList>;
|
|
51
|
+
get(rowId: number): Promise<Row>;
|
|
52
|
+
insert(data: Record<string, unknown>): Promise<Row>;
|
|
53
|
+
update(rowId: number, data: Record<string, unknown>): Promise<Row>;
|
|
54
|
+
remove(rowId: number): Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
declare function isLoggedIn(): boolean;
|
|
57
|
+
declare function logout(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Direct, scriptable registration call — for CI/setup scripts, not the
|
|
60
|
+
* interactive connect flow (that's handled entirely by mesync's own hosted
|
|
61
|
+
* authorize page now; see `login`/`loginPopup`'s `manifest` option). Takes
|
|
62
|
+
* a bearer token the caller already obtained however they like (e.g. their
|
|
63
|
+
* own `POST /api/auth/login` call) — this function does no credential
|
|
64
|
+
* management of its own.
|
|
65
|
+
*
|
|
66
|
+
* Safe to call any number of times: a new `uniqueId` creates the app; the
|
|
67
|
+
* same owner resubmitting diffs tables and applies only safe schema changes
|
|
68
|
+
* (new tables/columns, safe retypes); a `uniqueId` already owned by someone
|
|
69
|
+
* else throws MesyncUniqueIdConflictError; a manifest that shares no tables
|
|
70
|
+
* at all with what's already registered under your own `uniqueId` throws
|
|
71
|
+
* MesyncManifestDivergedError unless `force: true` is passed.
|
|
72
|
+
*/
|
|
73
|
+
declare function registerManifest(manifest: MesyncManifest, token: string, options?: {
|
|
74
|
+
force?: boolean;
|
|
75
|
+
}): Promise<MesyncManifestResult>;
|
|
76
|
+
declare const mesync: {
|
|
77
|
+
configure: typeof configure;
|
|
78
|
+
login: typeof login;
|
|
79
|
+
loginPopup: typeof loginPopup;
|
|
80
|
+
handleCallback: typeof handleCallback;
|
|
81
|
+
table: typeof table;
|
|
82
|
+
isLoggedIn: typeof isLoggedIn;
|
|
83
|
+
logout: typeof logout;
|
|
84
|
+
registerManifest: typeof registerManifest;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Identity function — its only purpose is type inference/checking at the call
|
|
88
|
+
* site without an explicit `: MesyncManifest` annotation (same trick as Vite's
|
|
89
|
+
* `defineConfig`). No runtime validation happens here; the server is the
|
|
90
|
+
* source of truth for that. */
|
|
91
|
+
declare function defineManifest(manifest: MesyncManifest): MesyncManifest;
|
|
92
|
+
|
|
93
|
+
export { MesyncAuthError, MesyncConfig, MesyncManifest, MesyncManifestDivergedError, MesyncManifestResult, MesyncUniqueIdConflictError, Row, RowList, ScopeRequest, defineManifest, mesync };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MesyncAuthError,
|
|
3
|
+
MesyncManifestDivergedError,
|
|
4
|
+
MesyncUniqueIdConflictError,
|
|
5
|
+
mesync
|
|
6
|
+
} from "./chunk-YPMJ6DJU.js";
|
|
7
|
+
|
|
8
|
+
// src/defineManifest.ts
|
|
9
|
+
function defineManifest(manifest) {
|
|
10
|
+
return manifest;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
MesyncAuthError,
|
|
14
|
+
MesyncManifestDivergedError,
|
|
15
|
+
MesyncUniqueIdConflictError,
|
|
16
|
+
defineManifest,
|
|
17
|
+
mesync
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/defineManifest.ts"],"sourcesContent":["import type { MesyncManifest } from \"./types\";\n\n/** Identity function — its only purpose is type inference/checking at the call\n * site without an explicit `: MesyncManifest` annotation (same trick as Vite's\n * `defineConfig`). No runtime validation happens here; the server is the\n * source of truth for that. */\nexport function defineManifest(manifest: MesyncManifest): MesyncManifest {\n return manifest;\n}\n"],"mappings":";;;;;;;;AAMO,SAAS,eAAe,UAA0C;AACvE,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
type Permission = "read" | "write";
|
|
2
|
+
interface ScopeRequest {
|
|
3
|
+
namespace: string;
|
|
4
|
+
table: string;
|
|
5
|
+
permission: Permission;
|
|
6
|
+
/** Shown to the user on the consent screen, explaining why this app wants this table/permission. */
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
interface MesyncConfig {
|
|
10
|
+
/** Origin the mesync app is deployed at, e.g. "https://mesync.example.com" (no trailing slash).
|
|
11
|
+
* Both the consent screen (`/oauth/authorize`) and the API (`/api/...`) are assumed to live here. */
|
|
12
|
+
mesyncUrl: string;
|
|
13
|
+
uniqueId: string;
|
|
14
|
+
/** Must exactly match one of the redirect_uris registered for this app. */
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
}
|
|
17
|
+
interface Row {
|
|
18
|
+
id: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
interface RowList {
|
|
24
|
+
items: Row[];
|
|
25
|
+
total: number;
|
|
26
|
+
page: number;
|
|
27
|
+
page_size: number;
|
|
28
|
+
pages: number;
|
|
29
|
+
}
|
|
30
|
+
type MesyncColumnType = "string" | "number" | "boolean" | "timestamp" | "json";
|
|
31
|
+
/** "required implies default" encoded as a discriminated union — a required
|
|
32
|
+
* column with no default simply fails to typecheck. */
|
|
33
|
+
type MesyncManifestColumn = {
|
|
34
|
+
name: string;
|
|
35
|
+
type: MesyncColumnType;
|
|
36
|
+
required: true;
|
|
37
|
+
default: unknown;
|
|
38
|
+
} | {
|
|
39
|
+
name: string;
|
|
40
|
+
type: MesyncColumnType;
|
|
41
|
+
required?: false;
|
|
42
|
+
default?: unknown;
|
|
43
|
+
};
|
|
44
|
+
interface MesyncManifestTable {
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
columns: MesyncManifestColumn[];
|
|
48
|
+
}
|
|
49
|
+
interface MesyncManifest {
|
|
50
|
+
/** Developer-chosen, stable, globally unique — the sole public identifier
|
|
51
|
+
* for this app. Immutable once registered; check availability first with
|
|
52
|
+
* mesync's `GET /api/catalog/unique-id-available`. */
|
|
53
|
+
uniqueId: string;
|
|
54
|
+
name: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
redirectUris: string[];
|
|
57
|
+
tables: MesyncManifestTable[];
|
|
58
|
+
}
|
|
59
|
+
interface MesyncManifestResult {
|
|
60
|
+
status: "created" | "confirmed" | "updated";
|
|
61
|
+
migrationsApplied: Record<string, number>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type { MesyncManifest as M, RowList as R, ScopeRequest as S, MesyncConfig as a, Row as b, MesyncManifestResult as c, MesyncColumnType as d, MesyncManifestColumn as e, MesyncManifestTable as f };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mesyncapp/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Client library for storing your app's data in the user's personal mesync backend instead of localStorage.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/senluchen2015/mesync/tree/main/sdk#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/senluchen2015/mesync.git",
|
|
10
|
+
"directory": "sdk"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./elements": {
|
|
25
|
+
"types": "./dist/elements/index.d.ts",
|
|
26
|
+
"import": "./dist/elements/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./elements/react": {
|
|
29
|
+
"types": "./dist/elements/react.d.ts",
|
|
30
|
+
"import": "./dist/elements/react.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "bunx --bun tsup",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"react": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "^19.2.0",
|
|
51
|
+
"react": "^19.2.0",
|
|
52
|
+
"tsup": "^8.3.5",
|
|
53
|
+
"typescript": "~5.7.0"
|
|
54
|
+
}
|
|
55
|
+
}
|