@clubays/clubgate-sdk 0.1.1 → 0.2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @clubays/clubgate-sdk
2
2
 
3
- SDK for building Clubays clubgate applications.
3
+ SDK for building Clubgate applications that run inside Clubays.
4
4
 
5
5
  ## Install
6
6
 
@@ -11,15 +11,82 @@ pnpm add @clubays/clubgate-sdk
11
11
  ## Use
12
12
 
13
13
  ```js
14
- import { clubays, init } from "@clubays/clubgate-sdk";
14
+ import { clubgate, init } from "@clubays/clubgate-sdk";
15
15
 
16
- init({ hostOrigin: import.meta.env.VITE_HOST_ORIGIN }); // optional; defaults to https://www.clubays.com
16
+ // optional; defaults to https://www.clubays.com
17
+ init({ hostOrigin: import.meta.env.VITE_HOST_ORIGIN });
17
18
 
18
- const user = await clubays.user.get();
19
- const profile = await clubays.profile.get();
19
+ const user = await clubgate.user.get();
20
+ // { linkId, name, username, profileimage }
21
+
22
+ const profile = await clubgate.profile.get();
23
+ // { name, username, profileimage, coverimage, bio, description, links }
24
+ ```
25
+
26
+ Each method needs a scope that your app requested in `clubgate.json` and a reviewer approved:
27
+
28
+ | Method | Scope |
29
+ | ------------------------- | -------------- |
30
+ | `clubgate.user.get()` | `user.read` |
31
+ | `clubgate.profile.get()` | `profile.read` |
32
+ | `clubgate.link.status()` | none |
33
+ | `clubgate.link.request()` | none |
34
+
35
+ ## Identity: use `linkId`, not `username`
36
+
37
+ `linkId` is a stable, per-app identifier for the user. Key your data on it. Usernames can change, and an old name can later belong to someone else, so use `username` for display only. Re-read `user.get()` each session instead of caching names.
38
+
39
+ The internal profile id is never exposed to apps.
40
+
41
+ ## Sign-in and consent
42
+
43
+ Set `"access": "signin"` in `clubgate.json` to require users to connect your app. Clubays shows the consent screen itself, and your app cannot skip or fake it.
44
+
45
+ ```js
46
+ const status = await clubgate.link.status();
47
+ // { linked, access, scopes }
48
+
49
+ if (!status.linked) {
50
+ try {
51
+ await clubgate.link.request(); // opens the consent screen, waits for the user
52
+ } catch (e) {
53
+ if (e.code === "consent_declined") {
54
+ // show a "connect to continue" screen
55
+ }
56
+ }
57
+ }
20
58
  ```
21
59
 
22
- Errors are `ClubaysError` with a `code`: `scope_denied`, `not_embedded`, `timeout`, `app_suspended`, `unknown_method`.
60
+ With `"access": "open"` (the default), the app works without linking, and `user.get()` returns `linkId: null` until the user connects.
61
+
62
+ ## Errors
63
+
64
+ Errors are `ClubgateError` with a `code`:
65
+
66
+ | Code | Meaning |
67
+ | ------------------ | ----------------------------------------------------- |
68
+ | `scope_denied` | The scope was not requested, approved or consented to |
69
+ | `link_required` | The app needs the user to connect first |
70
+ | `link_revoked` | The user disconnected the app |
71
+ | `consent_declined` | The user declined the consent screen |
72
+ | `app_suspended` | The app was suspended |
73
+ | `invalid_params` | The request parameters were invalid |
74
+ | `rate_limited` | Too many requests, retry later |
75
+ | `unknown_method` | The method does not exist |
76
+ | `not_embedded` | The app was opened outside Clubays or the emulator |
77
+ | `timeout` | No response from the host |
78
+
79
+ ```js
80
+ import { ClubgateError } from "@clubays/clubgate-sdk";
81
+
82
+ try {
83
+ await clubgate.profile.get();
84
+ } catch (e) {
85
+ if (e instanceof ClubgateError && e.code === "scope_denied") {
86
+ /* ... */
87
+ }
88
+ }
89
+ ```
23
90
 
24
91
  ## Local development
25
92
 
@@ -27,7 +94,13 @@ Errors are `ClubaysError` with a `code`: `scope_denied`, `not_embedded`, `timeou
27
94
  pnpm clubgate-emulator --app http://localhost:5174
28
95
  ```
29
96
 
30
- Set `VITE_HOST_ORIGIN=http://localhost:5199` while using the emulator.
97
+ Open the URL it prints, and set `VITE_HOST_ORIGIN=http://localhost:5199` while using the emulator.
98
+
99
+ Edit `emulator/mock-data.json` to test different situations:
100
+
101
+ - `"access": "signin"` shows the consent screen on load
102
+ - remove a scope from `scopes` to test `scope_denied`
103
+ - `"linked": true` starts the user as already connected
31
104
 
32
105
  ## Package for upload
33
106
 
@@ -36,4 +109,8 @@ pnpm build
36
109
  pnpm clubgate-pack
37
110
  ```
38
111
 
39
- Creates `<id>-<version>.zip` from `dist/` and `clubgate.json`.
112
+ Creates `<id>-<version>.zip` from `dist/` and `clubgate.json`. Upload the zip through the developer portal.
113
+
114
+ ## Notes
115
+
116
+ - `clubays` and `ClubaysError` still exist as deprecated aliases of `clubgate` and `ClubgateError`. They will be removed in 1.0.
package/bin/emulator.js CHANGED
@@ -1,50 +1,50 @@
1
- #!/usr/bin/env node
2
- import http from "node:http";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const root = path.join(
8
- path.dirname(fileURLToPath(import.meta.url)),
9
- "..",
10
- "emulator",
11
- );
12
- const args = process.argv.slice(2);
13
- const get = (flag, dflt) => {
14
- const i = args.indexOf(flag);
15
- return i > -1 ? args[i + 1] : dflt;
16
- };
17
- const port = Number(get("--port", 5199));
18
- const app = get("--app", "http://localhost:5174");
19
-
20
- const types = {
21
- ".html": "text/html",
22
- ".js": "text/javascript",
23
- ".json": "application/json",
24
- };
25
-
26
- http
27
- .createServer((req, res) => {
28
- const name = new URL(req.url, "http://x").pathname;
29
- const file = path.join(
30
- root,
31
- name === "/" ? "index.html" : path.normalize(name),
32
- );
33
- if (
34
- !file.startsWith(root) ||
35
- !fs.existsSync(file) ||
36
- fs.statSync(file).isDirectory()
37
- ) {
38
- res.writeHead(404).end("not found");
39
- return;
40
- }
41
- res.writeHead(200, {
42
- "content-type": types[path.extname(file)] || "application/octet-stream",
43
- });
44
- fs.createReadStream(file).pipe(res);
45
- })
46
- .listen(port, () => {
47
- console.log(
48
- `Clubgate emulator: http://localhost:${port}/?app=${encodeURIComponent(app)}`,
49
- );
50
- });
1
+ #!/usr/bin/env node
2
+ import http from "node:http";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = path.join(
8
+ path.dirname(fileURLToPath(import.meta.url)),
9
+ "..",
10
+ "emulator",
11
+ );
12
+ const args = process.argv.slice(2);
13
+ const get = (flag, dflt) => {
14
+ const i = args.indexOf(flag);
15
+ return i > -1 ? args[i + 1] : dflt;
16
+ };
17
+ const port = Number(get("--port", 5199));
18
+ const app = get("--app", "http://localhost:5174");
19
+
20
+ const types = {
21
+ ".html": "text/html",
22
+ ".js": "text/javascript",
23
+ ".json": "application/json",
24
+ };
25
+
26
+ http
27
+ .createServer((req, res) => {
28
+ const name = new URL(req.url, "http://x").pathname;
29
+ const file = path.join(
30
+ root,
31
+ name === "/" ? "index.html" : path.normalize(name),
32
+ );
33
+ if (
34
+ !file.startsWith(root) ||
35
+ !fs.existsSync(file) ||
36
+ fs.statSync(file).isDirectory()
37
+ ) {
38
+ res.writeHead(404).end("not found");
39
+ return;
40
+ }
41
+ res.writeHead(200, {
42
+ "content-type": types[path.extname(file)] || "application/octet-stream",
43
+ });
44
+ fs.createReadStream(file).pipe(res);
45
+ })
46
+ .listen(port, () => {
47
+ console.log(
48
+ `Clubgate emulator: http://localhost:${port}/?app=${encodeURIComponent(app)}`,
49
+ );
50
+ });
package/bin/pack.js CHANGED
@@ -1,86 +1,86 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import archiver from "archiver";
5
-
6
- const MAX_ZIP_BYTES = 20 * 1024 * 1024; // 20 MB
7
- const SCOPES = ["user.read", "profile.read"]; // keep in sync with the SDK's SCOPES
8
- const ID_RE = /^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;
9
- const VERSION_RE = /^\d+\.\d+\.\d+$/;
10
-
11
- const cwd = process.cwd();
12
- const fail = (msg) => {
13
- console.error(`\n x ${msg}\n`);
14
- process.exit(1);
15
- };
16
-
17
- // 1. build output must exist
18
- const dist = path.join(cwd, "dist");
19
- if (!fs.existsSync(path.join(dist, "index.html"))) {
20
- fail("dist/index.html not found. Run your build first (pnpm build).");
21
- }
22
-
23
- // 2. manifest must exist and be valid
24
- const manifestPath = path.join(cwd, "clubgate.json");
25
- if (!fs.existsSync(manifestPath))
26
- fail("clubgate.json not found in the project root.");
27
-
28
- let manifest;
29
- try {
30
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
31
- } catch {
32
- fail("clubgate.json is not valid JSON.");
33
- }
34
-
35
- const errors = [];
36
- if (manifest.schemaVersion !== 1) errors.push("schemaVersion must be 1");
37
- if (!ID_RE.test(manifest.id || ""))
38
- errors.push("id must be lowercase letters, numbers, hyphens (3-32 chars)");
39
- if (!manifest.name || manifest.name.length > 50)
40
- errors.push("name is required (max 50 chars)");
41
- if (!VERSION_RE.test(manifest.version || ""))
42
- errors.push("version must look like 0.1.0");
43
- if (!Array.isArray(manifest.scopes)) {
44
- errors.push("scopes must be an array");
45
- } else {
46
- for (const s of manifest.scopes) {
47
- if (!SCOPES.includes(s)) errors.push(`unknown scope: ${s}`);
48
- }
49
- }
50
- if (errors.length)
51
- fail("clubgate.json problems:\n - " + errors.join("\n - "));
52
-
53
- // 3. refuse files that must never ship (secrets, source maps are fine to drop)
54
- const BLOCKED = [/^\.env/, /\.pem$/, /\.key$/, /^node_modules$/];
55
- const walk = (dir) =>
56
- fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
57
- const full = path.join(dir, e.name);
58
- if (BLOCKED.some((re) => re.test(e.name)))
59
- fail(`dist contains a blocked file: ${path.relative(cwd, full)}`);
60
- return e.isDirectory() ? walk(full) : [full];
61
- });
62
- walk(dist);
63
-
64
- // 4. zip dist/ contents at the zip root, plus clubgate.json
65
- const outName = `${manifest.id}-${manifest.version}.zip`;
66
- const outPath = path.join(cwd, outName);
67
- const output = fs.createWriteStream(outPath);
68
- const archive = archiver("zip", { zlib: { level: 9 } });
69
-
70
- output.on("close", () => {
71
- const size = archive.pointer();
72
- if (size > MAX_ZIP_BYTES) {
73
- fs.unlinkSync(outPath);
74
- fail(
75
- `zip is ${(size / 1048576).toFixed(1)} MB, over the ${MAX_ZIP_BYTES / 1048576} MB limit.`,
76
- );
77
- }
78
- console.log(`\n ok ${outName} (${(size / 1024).toFixed(1)} KB)`);
79
- console.log(" Upload this file to Clubays.\n");
80
- });
81
- archive.on("error", (e) => fail(e.message));
82
-
83
- archive.pipe(output);
84
- archive.directory(dist, false);
85
- archive.file(manifestPath, { name: "clubgate.json" });
86
- archive.finalize();
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import archiver from "archiver";
5
+
6
+ const MAX_ZIP_BYTES = 20 * 1024 * 1024; // 20 MB
7
+ const SCOPES = ["user.read", "profile.read"]; // keep in sync with the SDK's SCOPES
8
+ const ID_RE = /^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;
9
+ const VERSION_RE = /^\d+\.\d+\.\d+$/;
10
+
11
+ const cwd = process.cwd();
12
+ const fail = (msg) => {
13
+ console.error(`\n x ${msg}\n`);
14
+ process.exit(1);
15
+ };
16
+
17
+ // 1. build output must exist
18
+ const dist = path.join(cwd, "dist");
19
+ if (!fs.existsSync(path.join(dist, "index.html"))) {
20
+ fail("dist/index.html not found. Run your build first (pnpm build).");
21
+ }
22
+
23
+ // 2. manifest must exist and be valid
24
+ const manifestPath = path.join(cwd, "clubgate.json");
25
+ if (!fs.existsSync(manifestPath))
26
+ fail("clubgate.json not found in the project root.");
27
+
28
+ let manifest;
29
+ try {
30
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
31
+ } catch {
32
+ fail("clubgate.json is not valid JSON.");
33
+ }
34
+
35
+ const errors = [];
36
+ if (manifest.schemaVersion !== 1) errors.push("schemaVersion must be 1");
37
+ if (!ID_RE.test(manifest.id || ""))
38
+ errors.push("id must be lowercase letters, numbers, hyphens (3-32 chars)");
39
+ if (!manifest.name || manifest.name.length > 50)
40
+ errors.push("name is required (max 50 chars)");
41
+ if (!VERSION_RE.test(manifest.version || ""))
42
+ errors.push("version must look like 0.1.0");
43
+ if (!Array.isArray(manifest.scopes)) {
44
+ errors.push("scopes must be an array");
45
+ } else {
46
+ for (const s of manifest.scopes) {
47
+ if (!SCOPES.includes(s)) errors.push(`unknown scope: ${s}`);
48
+ }
49
+ }
50
+ if (errors.length)
51
+ fail("clubgate.json problems:\n - " + errors.join("\n - "));
52
+
53
+ // 3. refuse files that must never ship (secrets, source maps are fine to drop)
54
+ const BLOCKED = [/^\.env/, /\.pem$/, /\.key$/, /^node_modules$/];
55
+ const walk = (dir) =>
56
+ fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
57
+ const full = path.join(dir, e.name);
58
+ if (BLOCKED.some((re) => re.test(e.name)))
59
+ fail(`dist contains a blocked file: ${path.relative(cwd, full)}`);
60
+ return e.isDirectory() ? walk(full) : [full];
61
+ });
62
+ walk(dist);
63
+
64
+ // 4. zip dist/ contents at the zip root, plus clubgate.json
65
+ const outName = `${manifest.id}-${manifest.version}.zip`;
66
+ const outPath = path.join(cwd, outName);
67
+ const output = fs.createWriteStream(outPath);
68
+ const archive = archiver("zip", { zlib: { level: 9 } });
69
+
70
+ output.on("close", () => {
71
+ const size = archive.pointer();
72
+ if (size > MAX_ZIP_BYTES) {
73
+ fs.unlinkSync(outPath);
74
+ fail(
75
+ `zip is ${(size / 1048576).toFixed(1)} MB, over the ${MAX_ZIP_BYTES / 1048576} MB limit.`,
76
+ );
77
+ }
78
+ console.log(`\n ok ${outName} (${(size / 1024).toFixed(1)} KB)`);
79
+ console.log(" Upload this file to Clubays.\n");
80
+ });
81
+ archive.on("error", (e) => fail(e.message));
82
+
83
+ archive.pipe(output);
84
+ archive.directory(dist, false);
85
+ archive.file(manifestPath, { name: "clubgate.json" });
86
+ archive.finalize();
package/dist/index.cjs CHANGED
@@ -21,14 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ClubaysError: () => ClubaysError,
24
+ ClubgateError: () => ClubgateError,
24
25
  clubays: () => clubays,
26
+ clubgate: () => clubgate,
25
27
  init: () => init
26
28
  });
27
29
  module.exports = __toCommonJS(index_exports);
28
- var ClubaysError = class extends Error {
30
+ var ClubgateError = class extends Error {
29
31
  constructor(code) {
30
32
  super(code);
31
- this.name = "ClubaysError";
33
+ this.name = "ClubgateError";
32
34
  this.code = code;
33
35
  }
34
36
  };
@@ -62,11 +64,11 @@ function onMessage(e) {
62
64
  if (!p) return;
63
65
  pending.delete(id);
64
66
  clearTimeout(p.timer);
65
- error ? p.reject(new ClubaysError(error)) : p.resolve(result);
67
+ error ? p.reject(new ClubgateError(error)) : p.resolve(result);
66
68
  }
67
- function request(method, params) {
69
+ function request(method, params, opts = {}) {
68
70
  if (typeof window === "undefined" || window.parent === window) {
69
- return Promise.reject(new ClubaysError("not_embedded"));
71
+ return Promise.reject(new ClubgateError("not_embedded"));
70
72
  }
71
73
  if (!listening) {
72
74
  window.addEventListener("message", onMessage);
@@ -75,8 +77,8 @@ function request(method, params) {
75
77
  return new Promise((resolve, reject) => {
76
78
  const id = ++seq;
77
79
  const timer = setTimeout(() => {
78
- if (pending.delete(id)) reject(new ClubaysError("timeout"));
79
- }, timeoutMs);
80
+ if (pending.delete(id)) reject(new ClubgateError("timeout"));
81
+ }, opts.timeoutMs ?? timeoutMs);
80
82
  pending.set(id, { resolve, reject, timer });
81
83
  window.parent.postMessage(
82
84
  { v: PROTOCOL_VERSION, id, method, params },
@@ -84,7 +86,19 @@ function request(method, params) {
84
86
  );
85
87
  });
86
88
  }
87
- var clubays = {
89
+ var clubgate = {
90
+ link: {
91
+ /** Is this user linked to the app, and with which scopes? */
92
+ status: () => request("link.status"),
93
+ /**
94
+ * Ask the host to show the consent screen. Resolves when the user accepts,
95
+ * rejects with `consent_declined` if they decline. The app cannot skip it.
96
+ * Waits for the user, so it uses a long timeout.
97
+ */
98
+ request: () => request("link.request", void 0, {
99
+ timeoutMs: 12e4
100
+ })
101
+ },
88
102
  user: {
89
103
  get: () => request("user.get")
90
104
  },
@@ -92,9 +106,13 @@ var clubays = {
92
106
  get: () => request("profile.get")
93
107
  }
94
108
  };
109
+ var clubays = clubgate;
110
+ var ClubaysError = ClubgateError;
95
111
  // Annotate the CommonJS export names for ESM import in node:
96
112
  0 && (module.exports = {
97
113
  ClubaysError,
114
+ ClubgateError,
98
115
  clubays,
116
+ clubgate,
99
117
  init
100
118
  });
package/dist/index.d.cts CHANGED
@@ -1,19 +1,28 @@
1
- interface ClubaysUser {
2
- profile_id: string;
1
+ interface ClubgateUser {
2
+ /** Stable per-app identity. null when the app is open and the user hasn't linked. */
3
+ linkId: string | null;
3
4
  name: string;
4
5
  username: string;
5
6
  profileimage: string | null;
6
7
  }
7
- interface ClubaysProfile extends ClubaysUser {
8
+ interface ClubgateProfile {
9
+ name: string;
10
+ username: string;
11
+ profileimage: string | null;
12
+ coverimage: string | null;
8
13
  bio: string | null;
9
14
  description: string | null;
10
- coverimage: string | null;
11
15
  links: string[] | null;
12
16
  }
13
- type ClubaysErrorCode = "scope_denied" | "unknown_method" | "app_suspended" | "rate_limited" | "timeout" | "not_embedded" | "failed" | (string & {});
14
- declare class ClubaysError extends Error {
15
- code: ClubaysErrorCode;
16
- constructor(code: ClubaysErrorCode);
17
+ interface LinkStatus {
18
+ linked: boolean;
19
+ access: "open" | "signin";
20
+ scopes: string[];
21
+ }
22
+ type ClubgateErrorCode = "scope_denied" | "unknown_method" | "app_suspended" | "link_required" | "link_revoked" | "consent_declined" | "invalid_params" | "rate_limited" | "timeout" | "not_embedded" | "failed" | (string & {});
23
+ declare class ClubgateError extends Error {
24
+ code: ClubgateErrorCode;
25
+ constructor(code: ClubgateErrorCode);
17
26
  }
18
27
  interface InitOptions {
19
28
  /** Origin of the Clubays host page. Defaults to production. */
@@ -22,13 +31,56 @@ interface InitOptions {
22
31
  timeoutMs?: number;
23
32
  }
24
33
  declare function init(options?: InitOptions): void;
34
+ declare const clubgate: {
35
+ link: {
36
+ /** Is this user linked to the app, and with which scopes? */
37
+ status: () => Promise<LinkStatus>;
38
+ /**
39
+ * Ask the host to show the consent screen. Resolves when the user accepts,
40
+ * rejects with `consent_declined` if they decline. The app cannot skip it.
41
+ * Waits for the user, so it uses a long timeout.
42
+ */
43
+ request: () => Promise<{
44
+ linked: boolean;
45
+ }>;
46
+ };
47
+ user: {
48
+ get: () => Promise<ClubgateUser>;
49
+ };
50
+ profile: {
51
+ get: () => Promise<ClubgateProfile>;
52
+ };
53
+ };
54
+ /** @deprecated use `clubgate` */
25
55
  declare const clubays: {
56
+ link: {
57
+ /** Is this user linked to the app, and with which scopes? */
58
+ status: () => Promise<LinkStatus>;
59
+ /**
60
+ * Ask the host to show the consent screen. Resolves when the user accepts,
61
+ * rejects with `consent_declined` if they decline. The app cannot skip it.
62
+ * Waits for the user, so it uses a long timeout.
63
+ */
64
+ request: () => Promise<{
65
+ linked: boolean;
66
+ }>;
67
+ };
26
68
  user: {
27
- get: () => Promise<ClubaysUser>;
69
+ get: () => Promise<ClubgateUser>;
28
70
  };
29
71
  profile: {
30
- get: () => Promise<ClubaysProfile>;
72
+ get: () => Promise<ClubgateProfile>;
31
73
  };
32
74
  };
75
+ /** @deprecated use `ClubgateError` */
76
+ declare const ClubaysError: typeof ClubgateError;
77
+ /** @deprecated use `ClubgateError` */
78
+ type ClubaysError = ClubgateError;
79
+ /** @deprecated use `ClubgateUser` */
80
+ type ClubaysUser = ClubgateUser;
81
+ /** @deprecated use `ClubgateProfile` */
82
+ type ClubaysProfile = ClubgateProfile;
83
+ /** @deprecated use `ClubgateErrorCode` */
84
+ type ClubaysErrorCode = ClubgateErrorCode;
33
85
 
34
- export { ClubaysError, type ClubaysErrorCode, type ClubaysProfile, type ClubaysUser, type InitOptions, clubays, init };
86
+ export { ClubaysError, type ClubaysErrorCode, type ClubaysProfile, type ClubaysUser, ClubgateError, type ClubgateErrorCode, type ClubgateProfile, type ClubgateUser, type InitOptions, type LinkStatus, clubays, clubgate, init };
package/dist/index.d.ts CHANGED
@@ -1,19 +1,28 @@
1
- interface ClubaysUser {
2
- profile_id: string;
1
+ interface ClubgateUser {
2
+ /** Stable per-app identity. null when the app is open and the user hasn't linked. */
3
+ linkId: string | null;
3
4
  name: string;
4
5
  username: string;
5
6
  profileimage: string | null;
6
7
  }
7
- interface ClubaysProfile extends ClubaysUser {
8
+ interface ClubgateProfile {
9
+ name: string;
10
+ username: string;
11
+ profileimage: string | null;
12
+ coverimage: string | null;
8
13
  bio: string | null;
9
14
  description: string | null;
10
- coverimage: string | null;
11
15
  links: string[] | null;
12
16
  }
13
- type ClubaysErrorCode = "scope_denied" | "unknown_method" | "app_suspended" | "rate_limited" | "timeout" | "not_embedded" | "failed" | (string & {});
14
- declare class ClubaysError extends Error {
15
- code: ClubaysErrorCode;
16
- constructor(code: ClubaysErrorCode);
17
+ interface LinkStatus {
18
+ linked: boolean;
19
+ access: "open" | "signin";
20
+ scopes: string[];
21
+ }
22
+ type ClubgateErrorCode = "scope_denied" | "unknown_method" | "app_suspended" | "link_required" | "link_revoked" | "consent_declined" | "invalid_params" | "rate_limited" | "timeout" | "not_embedded" | "failed" | (string & {});
23
+ declare class ClubgateError extends Error {
24
+ code: ClubgateErrorCode;
25
+ constructor(code: ClubgateErrorCode);
17
26
  }
18
27
  interface InitOptions {
19
28
  /** Origin of the Clubays host page. Defaults to production. */
@@ -22,13 +31,56 @@ interface InitOptions {
22
31
  timeoutMs?: number;
23
32
  }
24
33
  declare function init(options?: InitOptions): void;
34
+ declare const clubgate: {
35
+ link: {
36
+ /** Is this user linked to the app, and with which scopes? */
37
+ status: () => Promise<LinkStatus>;
38
+ /**
39
+ * Ask the host to show the consent screen. Resolves when the user accepts,
40
+ * rejects with `consent_declined` if they decline. The app cannot skip it.
41
+ * Waits for the user, so it uses a long timeout.
42
+ */
43
+ request: () => Promise<{
44
+ linked: boolean;
45
+ }>;
46
+ };
47
+ user: {
48
+ get: () => Promise<ClubgateUser>;
49
+ };
50
+ profile: {
51
+ get: () => Promise<ClubgateProfile>;
52
+ };
53
+ };
54
+ /** @deprecated use `clubgate` */
25
55
  declare const clubays: {
56
+ link: {
57
+ /** Is this user linked to the app, and with which scopes? */
58
+ status: () => Promise<LinkStatus>;
59
+ /**
60
+ * Ask the host to show the consent screen. Resolves when the user accepts,
61
+ * rejects with `consent_declined` if they decline. The app cannot skip it.
62
+ * Waits for the user, so it uses a long timeout.
63
+ */
64
+ request: () => Promise<{
65
+ linked: boolean;
66
+ }>;
67
+ };
26
68
  user: {
27
- get: () => Promise<ClubaysUser>;
69
+ get: () => Promise<ClubgateUser>;
28
70
  };
29
71
  profile: {
30
- get: () => Promise<ClubaysProfile>;
72
+ get: () => Promise<ClubgateProfile>;
31
73
  };
32
74
  };
75
+ /** @deprecated use `ClubgateError` */
76
+ declare const ClubaysError: typeof ClubgateError;
77
+ /** @deprecated use `ClubgateError` */
78
+ type ClubaysError = ClubgateError;
79
+ /** @deprecated use `ClubgateUser` */
80
+ type ClubaysUser = ClubgateUser;
81
+ /** @deprecated use `ClubgateProfile` */
82
+ type ClubaysProfile = ClubgateProfile;
83
+ /** @deprecated use `ClubgateErrorCode` */
84
+ type ClubaysErrorCode = ClubgateErrorCode;
33
85
 
34
- export { ClubaysError, type ClubaysErrorCode, type ClubaysProfile, type ClubaysUser, type InitOptions, clubays, init };
86
+ export { ClubaysError, type ClubaysErrorCode, type ClubaysProfile, type ClubaysUser, ClubgateError, type ClubgateErrorCode, type ClubgateProfile, type ClubgateUser, type InitOptions, type LinkStatus, clubays, clubgate, init };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.ts
2
- var ClubaysError = class extends Error {
2
+ var ClubgateError = class extends Error {
3
3
  constructor(code) {
4
4
  super(code);
5
- this.name = "ClubaysError";
5
+ this.name = "ClubgateError";
6
6
  this.code = code;
7
7
  }
8
8
  };
@@ -36,11 +36,11 @@ function onMessage(e) {
36
36
  if (!p) return;
37
37
  pending.delete(id);
38
38
  clearTimeout(p.timer);
39
- error ? p.reject(new ClubaysError(error)) : p.resolve(result);
39
+ error ? p.reject(new ClubgateError(error)) : p.resolve(result);
40
40
  }
41
- function request(method, params) {
41
+ function request(method, params, opts = {}) {
42
42
  if (typeof window === "undefined" || window.parent === window) {
43
- return Promise.reject(new ClubaysError("not_embedded"));
43
+ return Promise.reject(new ClubgateError("not_embedded"));
44
44
  }
45
45
  if (!listening) {
46
46
  window.addEventListener("message", onMessage);
@@ -49,8 +49,8 @@ function request(method, params) {
49
49
  return new Promise((resolve, reject) => {
50
50
  const id = ++seq;
51
51
  const timer = setTimeout(() => {
52
- if (pending.delete(id)) reject(new ClubaysError("timeout"));
53
- }, timeoutMs);
52
+ if (pending.delete(id)) reject(new ClubgateError("timeout"));
53
+ }, opts.timeoutMs ?? timeoutMs);
54
54
  pending.set(id, { resolve, reject, timer });
55
55
  window.parent.postMessage(
56
56
  { v: PROTOCOL_VERSION, id, method, params },
@@ -58,7 +58,19 @@ function request(method, params) {
58
58
  );
59
59
  });
60
60
  }
61
- var clubays = {
61
+ var clubgate = {
62
+ link: {
63
+ /** Is this user linked to the app, and with which scopes? */
64
+ status: () => request("link.status"),
65
+ /**
66
+ * Ask the host to show the consent screen. Resolves when the user accepts,
67
+ * rejects with `consent_declined` if they decline. The app cannot skip it.
68
+ * Waits for the user, so it uses a long timeout.
69
+ */
70
+ request: () => request("link.request", void 0, {
71
+ timeoutMs: 12e4
72
+ })
73
+ },
62
74
  user: {
63
75
  get: () => request("user.get")
64
76
  },
@@ -66,8 +78,12 @@ var clubays = {
66
78
  get: () => request("profile.get")
67
79
  }
68
80
  };
81
+ var clubays = clubgate;
82
+ var ClubaysError = ClubgateError;
69
83
  export {
70
84
  ClubaysError,
85
+ ClubgateError,
71
86
  clubays,
87
+ clubgate,
72
88
  init
73
89
  };
package/emulator/host.js CHANGED
@@ -4,22 +4,98 @@ const appOrigin = new URL(appUrl).origin;
4
4
 
5
5
  const data = await fetch("./mock-data.json").then((r) => r.json());
6
6
 
7
- const ROUTES = {
8
- "user.get": { scope: "user.read", data: data.user },
9
- "profile.get": { scope: "profile.read", data: data.profile },
7
+ // state the real host keeps per user
8
+ let linked = data.linked === true;
9
+
10
+ const LABELS = {
11
+ "user.read": "See your name, username and photo",
12
+ "profile.read": "See your public profile",
10
13
  };
11
14
 
12
15
  const frame = document.getElementById("app");
13
16
  frame.src = appUrl;
14
17
 
15
- window.addEventListener("message", (e) => {
18
+ // ── consent dialog (the app can never skip or fake this) ──
19
+ const waiters = [];
20
+ const dialog = document.createElement("div");
21
+ dialog.style.cssText =
22
+ "display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);align-items:center;justify-content:center;font-family:system-ui;z-index:10";
23
+ dialog.innerHTML = `
24
+ <div style="background:#fff;border-radius:16px;padding:20px;width:320px">
25
+ <p style="margin:0;font-size:14px;font-weight:600">Connect this app</p>
26
+ <p style="margin:6px 0 10px;font-size:12px;color:#64748b">This app will be able to:</p>
27
+ <ul id="scopes" style="margin:0;padding-left:18px;font-size:12px"></ul>
28
+ <div style="margin-top:16px;display:flex;justify-content:flex-end;gap:8px">
29
+ <button id="no" style="padding:6px 12px;font-size:12px">Not now</button>
30
+ <button id="yes" style="padding:6px 12px;font-size:12px;background:#7c3aed;color:#fff;border:0;border-radius:8px">Allow</button>
31
+ </div>
32
+ </div>`;
33
+ document.body.appendChild(dialog);
34
+ dialog.querySelector("#scopes").innerHTML = data.scopes
35
+ .map((s) => `<li>${LABELS[s] ?? s}</li>`)
36
+ .join("");
37
+
38
+ const showConsent = () =>
39
+ new Promise((resolve, reject) => {
40
+ waiters.push({ resolve, reject });
41
+ dialog.style.display = "flex";
42
+ });
43
+ const closeConsent = () => (dialog.style.display = "none");
44
+
45
+ dialog.querySelector("#yes").onclick = () => {
46
+ linked = true;
47
+ closeConsent();
48
+ waiters.splice(0).forEach((w) => w.resolve({ linked: true }));
49
+ };
50
+ dialog.querySelector("#no").onclick = () => {
51
+ closeConsent();
52
+ waiters.splice(0).forEach((w) => w.reject("consent_declined"));
53
+ };
54
+
55
+ // signin apps show consent on load, like the real host
56
+ if (data.access === "signin" && !linked) showConsent().catch(() => {});
57
+
58
+ // ── methods ──
59
+ const METHODS = {
60
+ "link.status": {
61
+ scopes: [],
62
+ run: () => ({
63
+ linked,
64
+ access: data.access,
65
+ scopes: linked ? data.scopes : [],
66
+ }),
67
+ },
68
+ "user.get": {
69
+ scopes: ["user.read"],
70
+ run: () => ({ linkId: linked ? data.linkId : null, ...data.user }),
71
+ },
72
+ "profile.get": {
73
+ scopes: ["profile.read"],
74
+ run: () => data.profile,
75
+ },
76
+ };
77
+
78
+ window.addEventListener("message", async (e) => {
16
79
  if (e.origin !== appOrigin || e.source !== frame.contentWindow) return;
17
80
  const { id, method } = e.data || {};
81
+ if (typeof method !== "string") return;
18
82
  const reply = (msg) => e.source.postMessage({ id, ...msg }, e.origin);
19
83
 
20
- const route = ROUTES[method];
21
- if (!route) return reply({ error: "unknown_method" });
22
- if (!data.scopes.includes(route.scope))
84
+ // handled by the host only, like the real one
85
+ if (method === "link.request") {
86
+ if (linked) return reply({ result: { linked: true } });
87
+ try {
88
+ return reply({ result: await showConsent() });
89
+ } catch (code) {
90
+ return reply({ error: code });
91
+ }
92
+ }
93
+
94
+ const def = METHODS[method];
95
+ if (!def) return reply({ error: "unknown_method" });
96
+ if (data.access === "signin" && !linked && method !== "link.status")
97
+ return reply({ error: "link_required" });
98
+ if (!def.scopes.every((s) => data.scopes.includes(s)))
23
99
  return reply({ error: "scope_denied" });
24
- reply({ result: route.data });
100
+ reply({ result: def.run() });
25
101
  });
@@ -1,13 +1,14 @@
1
1
  {
2
+ "access": "open",
3
+ "linked": false,
2
4
  "scopes": ["user.read", "profile.read"],
5
+ "linkId": "11111111-1111-4111-8111-111111111111",
3
6
  "user": {
4
- "profile_id": "mock0001",
5
7
  "name": "Test User",
6
8
  "username": "test_user",
7
9
  "profileimage": null
8
10
  },
9
11
  "profile": {
10
- "profile_id": "mock0001",
11
12
  "name": "Test User",
12
13
  "username": "test_user",
13
14
  "bio": "Mock bio",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubays/clubgate-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "SDK for building Clubays miniapps",
5
5
  "type": "module",
6
6
  "license": "MIT",