@storyshelf/design-figma 0.0.0-stage → 0.7.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Siddhant Gupta
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 CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # @storyshelf/design-figma
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Figma design provider for StoryShelf: validates Figma links, reads file/node metadata (name, version, last modified) and downloads rendered previews through the Figma REST API with a personal access token.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ nub add @storyshelf/design-figma
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm install @storyshelf/design-figma
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createShelfApp } from "@storyshelf/app";
21
+ import { createFigmaDesign } from "@storyshelf/design-figma";
22
+
23
+ const app = createShelfApp({
24
+ database,
25
+ storage,
26
+ testing: { runner: captureRunner },
27
+ design: [createFigmaDesign()],
28
+ config: {
29
+ secret: process.env.SHELF_SECRET, // also encrypts the Figma token
30
+ },
31
+ });
32
+ ```
33
+
34
+ Without a provider, StoryShelf extracts and shows no design links. With it, stories link to Figma through `parameters.storyshelf.figma` or the Storybook `design` addon, and project admins manage mappings and the access token under **Settings → Design**. See [Design links](https://storyshelf.js.org/guides/design-links/) for how stories, mappings and the access token work.
35
+
36
+ ## Let members connect their own account
37
+
38
+ Without a project token, a member can connect their own Figma account (like Jira or Linear integrations) and see previews, names and stale-design warnings with their own access. Register an OAuth app in Figma (**Settings → OAuth apps**; a private app works within your organization), add the redirect URL `<publicBaseUrl>/connections/figma/callback` and the `file_content:read` scope, then:
39
+
40
+ ```ts
41
+ import { createFigmaDesign, figmaOAuthFromEnv } from "@storyshelf/design-figma";
42
+
43
+ design: [createFigmaDesign({ oauth: figmaOAuthFromEnv() })], // FIGMA_CLIENT_ID / FIGMA_CLIENT_SECRET
44
+ ```
45
+
46
+ This needs authentication enabled and `config.secret` (it signs the connect flow and encrypts tokens). A project token, when set, always takes precedence; a member's token is used only while they view a page, and what it fetches is shown only to them.
47
+
48
+ ## Accepted links
49
+
50
+ `https://www.figma.com/{file|design|board|proto|make}/<key>/…` with an optional `node-id`. Embed, community and Figma Sites links are rejected.
package/dist/api.d.mts ADDED
@@ -0,0 +1,23 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DesignCallOptions, DesignClient, DesignMeta, DesignRef } from "@storyshelf/core/adapter/design";
4
+ import { Buffer } from "node:buffer";
5
+ //#region src/api.d.ts
6
+ /** A Figma client bound to a personal access token (or, with `authType: "oauth"`, a user's OAuth token). */
7
+ declare function createFigmaClient(credential: string, authType?: "token" | "oauth"): DesignClient;
8
+ /**
9
+ * Fetch metadata for several refs of one file in at most two requests: one
10
+ * `nodes?ids=a,b` call for every node ref and one depth-1 file call when the
11
+ * whole file is linked. API failures map to a state per ref.
12
+ */
13
+ declare function fetchFigmaMetas(token: string, fileKey: string, refs: readonly DesignRef[], options?: DesignCallOptions): Promise<Map<string, DesignMeta>>;
14
+ /**
15
+ * Render several nodes of one file to PNG with a single Images API call, then
16
+ * download each (Figma's signed URLs expire, so they are never hot-linked).
17
+ * Nodes Figma has no render for, or whose download fails, are omitted.
18
+ * Throws when the Images API call itself fails.
19
+ */
20
+ declare function fetchFigmaRenders(token: string, fileKey: string, nodeIds: readonly string[], options?: DesignCallOptions): Promise<Map<string, Buffer>>;
21
+ //#endregion
22
+ export { createFigmaClient, fetchFigmaMetas, fetchFigmaRenders };
23
+ //# sourceMappingURL=api.d.mts.map
package/dist/api.mjs ADDED
@@ -0,0 +1,131 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { HttpError, httpBytes, httpJson } from "@storyshelf/core/utils";
5
+ //#region src/api.ts
6
+ const FIGMA_API = "https://api.figma.com/v1";
7
+ /**
8
+ * Render scale: 2x keeps text legible next to retina screenshots without the
9
+ * 4x payload; renders are reference-only, never diffed.
10
+ */
11
+ const RENDER_SCALE = 2;
12
+ const EMPTY = {
13
+ name: null,
14
+ version: null,
15
+ lastModified: null
16
+ };
17
+ /** Marks an OAuth access token: sent as `Authorization: Bearer`, where a personal token uses `X-Figma-Token`. */
18
+ const BEARER = "Bearer ";
19
+ /** A Figma client bound to a personal access token (or, with `authType: "oauth"`, a user's OAuth token). */
20
+ function createFigmaClient(credential, authType = "token") {
21
+ const token = authType === "oauth" ? `${BEARER}${credential}` : credential;
22
+ return {
23
+ fetchMetas: async (fileKey, refs, options) => await fetchFigmaMetas(token, fileKey, refs, options),
24
+ fetchRenders: async (fileKey, nodeIds, options) => await fetchFigmaRenders(token, fileKey, nodeIds, options)
25
+ };
26
+ }
27
+ function requestOptions(token, timeoutMs, options) {
28
+ return {
29
+ headers: token.startsWith(BEARER) ? { Authorization: token } : { "X-Figma-Token": token },
30
+ timeoutMs,
31
+ retries: 2,
32
+ ...options.logger ? { logger: options.logger } : {},
33
+ ...options.signal ? { signal: options.signal } : {}
34
+ };
35
+ }
36
+ /**
37
+ * Fetch metadata for several refs of one file in at most two requests: one
38
+ * `nodes?ids=a,b` call for every node ref and one depth-1 file call when the
39
+ * whole file is linked. API failures map to a state per ref.
40
+ */
41
+ async function fetchFigmaMetas(token, fileKey, refs, options = {}) {
42
+ const ids = [...new Set(refs.flatMap((ref) => ref.nodeId ? [ref.nodeId] : []))];
43
+ const results = ids.length > 0 ? await fetchNodeMetas(token, fileKey, ids, options) : /* @__PURE__ */ new Map();
44
+ if (refs.some((ref) => !ref.nodeId)) results.set("", await fetchFileMeta(token, fileKey, options));
45
+ return results;
46
+ }
47
+ /** One `nodes?ids=a,b` request answering for every node id. */
48
+ async function fetchNodeMetas(token, fileKey, ids, options) {
49
+ const outcome = await fetchOrFail(`${FIGMA_API}/files/${encodeURIComponent(fileKey)}/nodes?ids=${encodeURIComponent(ids.join(","))}&depth=1`, token, options);
50
+ return new Map(ids.map((id) => [id, "meta" in outcome ? nodeMeta(outcome.meta, id) : outcome]));
51
+ }
52
+ async function fetchFileMeta(token, fileKey, options) {
53
+ const outcome = await fetchOrFail(`${FIGMA_API}/files/${encodeURIComponent(fileKey)}?depth=1`, token, options);
54
+ return "meta" in outcome ? fileMeta(outcome.meta) : outcome;
55
+ }
56
+ async function fetchOrFail(url, token, options) {
57
+ try {
58
+ return { meta: await httpJson(url, requestOptions(token, 1e4, options)) };
59
+ } catch (error) {
60
+ return failureMeta(error);
61
+ }
62
+ }
63
+ function fileMeta(body) {
64
+ return {
65
+ state: "ok",
66
+ name: body.name ?? null,
67
+ version: body.version ?? null,
68
+ lastModified: body.lastModified ?? null
69
+ };
70
+ }
71
+ function nodeMeta(body, id) {
72
+ const base = {
73
+ version: body.version ?? null,
74
+ lastModified: body.lastModified ?? null
75
+ };
76
+ const node = body.nodes?.[id];
77
+ if (!node) return {
78
+ state: "deleted",
79
+ name: null,
80
+ ...base
81
+ };
82
+ return {
83
+ state: "ok",
84
+ name: node.document?.name ?? body.name ?? null,
85
+ ...base
86
+ };
87
+ }
88
+ function failureMeta(error) {
89
+ if (error instanceof HttpError) {
90
+ if (error.status === 401) return {
91
+ state: "unauthorized",
92
+ ...EMPTY
93
+ };
94
+ if (error.status === 403) return {
95
+ state: "forbidden",
96
+ ...EMPTY
97
+ };
98
+ if (error.status === 404) return {
99
+ state: "deleted",
100
+ ...EMPTY
101
+ };
102
+ }
103
+ return {
104
+ state: "error",
105
+ ...EMPTY
106
+ };
107
+ }
108
+ /**
109
+ * Render several nodes of one file to PNG with a single Images API call, then
110
+ * download each (Figma's signed URLs expire, so they are never hot-linked).
111
+ * Nodes Figma has no render for, or whose download fails, are omitted.
112
+ * Throws when the Images API call itself fails.
113
+ */
114
+ async function fetchFigmaRenders(token, fileKey, nodeIds, options = {}) {
115
+ if (nodeIds.length === 0) return /* @__PURE__ */ new Map();
116
+ const ids = encodeURIComponent(nodeIds.join(","));
117
+ const body = await httpJson(`${FIGMA_API}/images/${encodeURIComponent(fileKey)}?ids=${ids}&format=png&scale=${RENDER_SCALE}`, requestOptions(token, 15e3, options));
118
+ const downloaded = await Promise.all(nodeIds.map(async (id) => {
119
+ const url = body.images?.[id];
120
+ return url ? [id, await download(url, options)] : null;
121
+ }));
122
+ return new Map(downloaded.filter((entry) => entry !== null));
123
+ }
124
+ async function download(url, options) {
125
+ const { headers: _headers, ...rest } = requestOptions("", 3e4, options);
126
+ return await httpBytes(url, rest);
127
+ }
128
+ //#endregion
129
+ export { createFigmaClient, fetchFigmaMetas, fetchFigmaRenders };
130
+
131
+ //# sourceMappingURL=api.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../src/api.ts"],"sourcesContent":["/** Figma REST client: batched metadata and render fetches with a personal access token, via the shared HTTP helper. */\nimport type {\n DesignCallOptions,\n DesignClient,\n DesignMeta,\n DesignRef,\n} from \"@storyshelf/core/adapter/design\";\nimport { HttpError, httpBytes, httpJson, type HttpRequestOptions } from \"@storyshelf/core/utils\";\nimport type { Buffer } from \"node:buffer\";\n\nconst FIGMA_API = \"https://api.figma.com/v1\";\n/**\n * Render scale: 2x keeps text legible next to retina screenshots without the\n * 4x payload; renders are reference-only, never diffed.\n */\nconst RENDER_SCALE = 2;\n\ninterface FileResponse {\n name?: string;\n version?: string;\n lastModified?: string;\n}\ninterface NodesResponse extends FileResponse {\n nodes?: Record<string, { document?: { name?: string } } | null>;\n}\ninterface ImagesResponse {\n images?: Record<string, string | null>;\n}\n\nconst EMPTY = { name: null, version: null, lastModified: null } as const;\n\n/** Marks an OAuth access token: sent as `Authorization: Bearer`, where a personal token uses `X-Figma-Token`. */\nconst BEARER = \"Bearer \";\n\n/** A Figma client bound to a personal access token (or, with `authType: \"oauth\"`, a user's OAuth token). */\nexport function createFigmaClient(\n credential: string,\n authType: \"token\" | \"oauth\" = \"token\",\n): DesignClient {\n const token = authType === \"oauth\" ? `${BEARER}${credential}` : credential;\n return {\n fetchMetas: async (fileKey, refs, options) =>\n await fetchFigmaMetas(token, fileKey, refs, options),\n fetchRenders: async (fileKey, nodeIds, options) =>\n await fetchFigmaRenders(token, fileKey, nodeIds, options),\n };\n}\n\nfunction requestOptions(\n token: string,\n timeoutMs: number,\n options: DesignCallOptions,\n): HttpRequestOptions {\n return {\n headers: token.startsWith(BEARER) ? { Authorization: token } : { \"X-Figma-Token\": token },\n timeoutMs,\n // `retries` is the total attempt count (first try included), so 2 = one retry; the shared default is 3.\n retries: 2,\n ...(options.logger ? { logger: options.logger } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n };\n}\n\n/**\n * Fetch metadata for several refs of one file in at most two requests: one\n * `nodes?ids=a,b` call for every node ref and one depth-1 file call when the\n * whole file is linked. API failures map to a state per ref.\n */\nexport async function fetchFigmaMetas(\n token: string,\n fileKey: string,\n refs: readonly DesignRef[],\n options: DesignCallOptions = {},\n): Promise<Map<string, DesignMeta>> {\n const ids = [...new Set(refs.flatMap((ref) => (ref.nodeId ? [ref.nodeId] : [])))];\n const results = ids.length > 0 ? await fetchNodeMetas(token, fileKey, ids, options) : new Map();\n if (refs.some((ref) => !ref.nodeId)) {\n results.set(\"\", await fetchFileMeta(token, fileKey, options));\n }\n return results;\n}\n\n/** One `nodes?ids=a,b` request answering for every node id. */\nasync function fetchNodeMetas(\n token: string,\n fileKey: string,\n ids: readonly string[],\n options: DesignCallOptions,\n): Promise<Map<string, DesignMeta>> {\n const key = encodeURIComponent(fileKey);\n const url = `${FIGMA_API}/files/${key}/nodes?ids=${encodeURIComponent(ids.join(\",\"))}&depth=1`;\n const outcome = await fetchOrFail<NodesResponse>(url, token, options);\n return new Map(ids.map((id) => [id, \"meta\" in outcome ? nodeMeta(outcome.meta, id) : outcome]));\n}\n\nasync function fetchFileMeta(\n token: string,\n fileKey: string,\n options: DesignCallOptions,\n): Promise<DesignMeta> {\n const url = `${FIGMA_API}/files/${encodeURIComponent(fileKey)}?depth=1`;\n const outcome = await fetchOrFail<FileResponse>(url, token, options);\n return \"meta\" in outcome ? fileMeta(outcome.meta) : outcome;\n}\n\ntype Fetched<T> = { meta: T } | DesignMeta;\n\nasync function fetchOrFail<T>(\n url: string,\n token: string,\n options: DesignCallOptions,\n): Promise<Fetched<T>> {\n try {\n return { meta: await httpJson<T>(url, requestOptions(token, 10_000, options)) };\n } catch (error) {\n return failureMeta(error);\n }\n}\n\nfunction fileMeta(body: FileResponse): DesignMeta {\n return {\n state: \"ok\",\n name: body.name ?? null,\n version: body.version ?? null,\n lastModified: body.lastModified ?? null,\n };\n}\n\nfunction nodeMeta(body: NodesResponse, id: string): DesignMeta {\n const base = { version: body.version ?? null, lastModified: body.lastModified ?? null };\n const node = body.nodes?.[id];\n if (!node) return { state: \"deleted\", name: null, ...base };\n return { state: \"ok\", name: node.document?.name ?? body.name ?? null, ...base };\n}\n\nfunction failureMeta(error: unknown): DesignMeta {\n if (error instanceof HttpError) {\n if (error.status === 401) return { state: \"unauthorized\", ...EMPTY };\n if (error.status === 403) return { state: \"forbidden\", ...EMPTY };\n if (error.status === 404) return { state: \"deleted\", ...EMPTY };\n }\n return { state: \"error\", ...EMPTY };\n}\n\n/**\n * Render several nodes of one file to PNG with a single Images API call, then\n * download each (Figma's signed URLs expire, so they are never hot-linked).\n * Nodes Figma has no render for, or whose download fails, are omitted.\n * Throws when the Images API call itself fails.\n */\nexport async function fetchFigmaRenders(\n token: string,\n fileKey: string,\n nodeIds: readonly string[],\n options: DesignCallOptions = {},\n): Promise<Map<string, Buffer>> {\n if (nodeIds.length === 0) return new Map();\n const ids = encodeURIComponent(nodeIds.join(\",\"));\n const body = await httpJson<ImagesResponse>(\n `${FIGMA_API}/images/${encodeURIComponent(fileKey)}?ids=${ids}&format=png&scale=${RENDER_SCALE}`,\n requestOptions(token, 15_000, options),\n );\n const downloaded = await Promise.all(\n nodeIds.map(async (id) => {\n const url = body.images?.[id];\n return url ? ([id, await download(url, options)] as const) : null;\n }),\n );\n return new Map(downloaded.filter((entry) => entry !== null));\n}\n\nasync function download(url: string, options: DesignCallOptions): Promise<Buffer> {\n const { headers: _headers, ...rest } = requestOptions(\"\", 30_000, options);\n return await httpBytes(url, rest);\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { FigmaOAuthOptions, createFigmaOAuth, figmaOAuthFromEnv } from "./oauth.mjs";
4
+ import { createFigmaClient, fetchFigmaMetas, fetchFigmaRenders } from "./api.mjs";
5
+ import { parseFigmaUrl } from "./url.mjs";
6
+ import { DesignProvider } from "@storyshelf/core/adapter/design";
7
+ //#region src/index.d.ts
8
+ /** Options of {@link createFigmaDesign}. */
9
+ interface FigmaDesignOptions {
10
+ /** Registered Figma OAuth app: lets members connect their own account when the project has no token. */
11
+ oauth?: FigmaOAuthOptions;
12
+ }
13
+ /** Figma design provider for StoryShelf: pass it as `design: [createFigmaDesign()]`. */
14
+ declare function createFigmaDesign(options?: FigmaDesignOptions): DesignProvider;
15
+ //#endregion
16
+ export { FigmaDesignOptions, type FigmaOAuthOptions, createFigmaClient, createFigmaDesign, createFigmaOAuth, fetchFigmaMetas, fetchFigmaRenders, figmaOAuthFromEnv, parseFigmaUrl };
17
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { createFigmaClient, fetchFigmaMetas, fetchFigmaRenders } from "./api.mjs";
5
+ import { getMetadata } from "./metadata.mjs";
6
+ import { createFigmaOAuth, figmaOAuthFromEnv } from "./oauth.mjs";
7
+ import { parseFigmaUrl } from "./url.mjs";
8
+ //#region src/index.ts
9
+ /** Figma design provider for StoryShelf: pass it as `design: [createFigmaDesign()]`. */
10
+ function createFigmaDesign(options = {}) {
11
+ return {
12
+ metadata: getMetadata(),
13
+ parseUrl: parseFigmaUrl,
14
+ create: ({ credential, authType }) => createFigmaClient(credential, authType),
15
+ ...options.oauth ? { oauth: createFigmaOAuth(options.oauth) } : {}
16
+ };
17
+ }
18
+ //#endregion
19
+ export { createFigmaClient, createFigmaDesign, createFigmaOAuth, fetchFigmaMetas, fetchFigmaRenders, figmaOAuthFromEnv, parseFigmaUrl };
20
+
21
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { DesignProvider } from \"@storyshelf/core/adapter/design\";\nimport { createFigmaClient } from \"./api.ts\";\nimport { getMetadata } from \"./metadata.ts\";\nimport { createFigmaOAuth, type FigmaOAuthOptions } from \"./oauth.ts\";\nimport { parseFigmaUrl } from \"./url.ts\";\n\n/** Options of {@link createFigmaDesign}. */\nexport interface FigmaDesignOptions {\n /** Registered Figma OAuth app: lets members connect their own account when the project has no token. */\n oauth?: FigmaOAuthOptions;\n}\n\n/** Figma design provider for StoryShelf: pass it as `design: [createFigmaDesign()]`. */\nexport function createFigmaDesign(options: FigmaDesignOptions = {}): DesignProvider {\n return {\n metadata: getMetadata(),\n parseUrl: parseFigmaUrl,\n create: ({ credential, authType }) => createFigmaClient(credential, authType),\n ...(options.oauth ? { oauth: createFigmaOAuth(options.oauth) } : {}),\n };\n}\n\nexport { createFigmaClient, fetchFigmaMetas, fetchFigmaRenders } from \"./api.ts\";\nexport { createFigmaOAuth, figmaOAuthFromEnv, type FigmaOAuthOptions } from \"./oauth.ts\";\nexport { parseFigmaUrl } from \"./url.ts\";\n"],"mappings":";;;;;;;;;AAaA,SAAgB,kBAAkB,UAA8B,CAAC,GAAmB;CAClF,OAAO;EACL,UAAU,YAAY;EACtB,UAAU;EACV,SAAS,EAAE,YAAY,eAAe,kBAAkB,YAAY,QAAQ;EAC5E,GAAI,QAAQ,QAAQ,EAAE,OAAO,iBAAiB,QAAQ,KAAK,EAAE,IAAI,CAAC;CACpE;AACF"}
@@ -0,0 +1,23 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/metadata.ts
5
+ /** Describe the Figma provider (name, version, credential copy). */
6
+ function getMetadata() {
7
+ return {
8
+ name: "Figma",
9
+ version: "0.7.0",
10
+ description: "Figma links, file metadata and rendered previews via the Figma REST API",
11
+ kind: "figma",
12
+ category: "design",
13
+ credentialLabel: "Figma access token",
14
+ credentialHelp: "A personal access token with read access (file content) lets StoryShelf show the design name and flag stale designs. Without one, buttons still deep-link to Figma. Figma tokens expire after at most 90 days — rotate before then.",
15
+ credentialPlaceholder: "figd_…",
16
+ urlHint: "https://www.figma.com/design/<file>?node-id=1-2",
17
+ credentialMaxAgeDays: 90
18
+ };
19
+ }
20
+ //#endregion
21
+ export { getMetadata };
22
+
23
+ //# sourceMappingURL=metadata.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.mjs","names":[],"sources":["../src/metadata.ts"],"sourcesContent":["import type { DesignProvider } from \"@storyshelf/core/adapter/design\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/** Describe the Figma provider (name, version, credential copy). */\nexport function getMetadata(): DesignProvider[\"metadata\"] {\n return {\n name: \"Figma\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"Figma links, file metadata and rendered previews via the Figma REST API\",\n kind: \"figma\",\n category: \"design\",\n credentialLabel: \"Figma access token\",\n credentialHelp:\n \"A personal access token with read access (file content) lets StoryShelf show the design name and flag stale designs. Without one, buttons still deep-link to Figma. Figma tokens expire after at most 90 days — rotate before then.\",\n credentialPlaceholder: \"figd_…\",\n urlHint: \"https://www.figma.com/design/<file>?node-id=1-2\",\n credentialMaxAgeDays: 90,\n };\n}\n"],"mappings":";;;;;AAKA,SAAgB,cAA0C;CACxD,OAAO;EACL,MAAM;EACN,SAAA;EACA,aAAa;EACb,MAAM;EACN,UAAU;EACV,iBAAiB;EACjB,gBACE;EACF,uBAAuB;EACvB,SAAS;EACT,sBAAsB;CACxB;AACF"}
@@ -0,0 +1,16 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { OAuthClient } from "@storyshelf/core/adapter/oauth";
4
+ //#region src/oauth.d.ts
5
+ /** Operator-registered Figma OAuth app (Figma → Settings → OAuth apps). */
6
+ interface FigmaOAuthOptions {
7
+ clientId: string;
8
+ clientSecret: string;
9
+ }
10
+ /** OAuth app credentials from `FIGMA_CLIENT_ID` / `FIGMA_CLIENT_SECRET`, or undefined when either is unset. */
11
+ declare function figmaOAuthFromEnv(env?: Record<string, string | undefined>): FigmaOAuthOptions | undefined;
12
+ /** Figma's {@link OAuthClient} for one registered OAuth app. */
13
+ declare function createFigmaOAuth(options: FigmaOAuthOptions): OAuthClient;
14
+ //#endregion
15
+ export { FigmaOAuthOptions, createFigmaOAuth, figmaOAuthFromEnv };
16
+ //# sourceMappingURL=oauth.d.mts.map
package/dist/oauth.mjs ADDED
@@ -0,0 +1,64 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { httpJson } from "@storyshelf/core/utils";
5
+ //#region src/oauth.ts
6
+ const AUTHORIZE_URL = "https://www.figma.com/oauth";
7
+ const TOKEN_URL = "https://api.figma.com/v1/oauth/token";
8
+ const REFRESH_URL = "https://api.figma.com/v1/oauth/refresh";
9
+ /** Read-only: file content (names, versions, renders). */
10
+ const SCOPE = "file_content:read";
11
+ /** Figma issues 90-day access tokens; used when a response omits `expires_in`. */
12
+ const DEFAULT_EXPIRES_S = 7776e3;
13
+ /** OAuth app credentials from `FIGMA_CLIENT_ID` / `FIGMA_CLIENT_SECRET`, or undefined when either is unset. */
14
+ function figmaOAuthFromEnv(env = process.env) {
15
+ const clientId = env["FIGMA_CLIENT_ID"];
16
+ const clientSecret = env["FIGMA_CLIENT_SECRET"];
17
+ return clientId && clientSecret ? {
18
+ clientId,
19
+ clientSecret
20
+ } : void 0;
21
+ }
22
+ /** Figma's {@link OAuthClient} for one registered OAuth app. */
23
+ function createFigmaOAuth(options) {
24
+ const basic = `Basic ${Buffer.from(`${options.clientId}:${options.clientSecret}`).toString("base64")}`;
25
+ const post = async (url, form, signal) => {
26
+ return toTokens(await httpJson(url, {
27
+ method: "POST",
28
+ headers: { Authorization: basic },
29
+ form,
30
+ retries: 1,
31
+ ...signal ? { signal } : {}
32
+ }));
33
+ };
34
+ return {
35
+ authorizeUrl: ({ state, redirectUri }) => {
36
+ const params = new URLSearchParams({
37
+ client_id: options.clientId,
38
+ redirect_uri: redirectUri,
39
+ scope: SCOPE,
40
+ state,
41
+ response_type: "code"
42
+ });
43
+ return `${AUTHORIZE_URL}?${params.toString()}`;
44
+ },
45
+ exchangeCode: async ({ code, redirectUri, signal }) => await post(TOKEN_URL, {
46
+ redirect_uri: redirectUri,
47
+ code,
48
+ grant_type: "authorization_code"
49
+ }, signal),
50
+ refresh: async ({ refreshToken, signal }) => await post(REFRESH_URL, { refresh_token: refreshToken }, signal)
51
+ };
52
+ }
53
+ function toTokens(response) {
54
+ const seconds = response.expires_in ?? DEFAULT_EXPIRES_S;
55
+ return {
56
+ accessToken: response.access_token,
57
+ refreshToken: response.refresh_token ?? null,
58
+ expiresAt: new Date(Date.now() + seconds * 1e3).toISOString()
59
+ };
60
+ }
61
+ //#endregion
62
+ export { createFigmaOAuth, figmaOAuthFromEnv };
63
+
64
+ //# sourceMappingURL=oauth.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.mjs","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["/** Figma OAuth 2 (authorization-code grant): consent URL, code exchange and refresh. */\nimport type { OAuthClient, OAuthTokens } from \"@storyshelf/core/adapter/oauth\";\nimport { httpJson } from \"@storyshelf/core/utils\";\n\n/** Operator-registered Figma OAuth app (Figma → Settings → OAuth apps). */\nexport interface FigmaOAuthOptions {\n clientId: string;\n clientSecret: string;\n}\n\nconst AUTHORIZE_URL = \"https://www.figma.com/oauth\";\nconst TOKEN_URL = \"https://api.figma.com/v1/oauth/token\";\nconst REFRESH_URL = \"https://api.figma.com/v1/oauth/refresh\";\n/** Read-only: file content (names, versions, renders). */\nconst SCOPE = \"file_content:read\";\n/** Figma issues 90-day access tokens; used when a response omits `expires_in`. */\nconst DEFAULT_EXPIRES_S = 90 * 24 * 60 * 60;\n\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in?: number;\n}\n\n/** OAuth app credentials from `FIGMA_CLIENT_ID` / `FIGMA_CLIENT_SECRET`, or undefined when either is unset. */\nexport function figmaOAuthFromEnv(\n env: Record<string, string | undefined> = process.env,\n): FigmaOAuthOptions | undefined {\n const clientId = env[\"FIGMA_CLIENT_ID\"];\n const clientSecret = env[\"FIGMA_CLIENT_SECRET\"];\n return clientId && clientSecret ? { clientId, clientSecret } : undefined;\n}\n\n/** Figma's {@link OAuthClient} for one registered OAuth app. */\nexport function createFigmaOAuth(options: FigmaOAuthOptions): OAuthClient {\n const basic = `Basic ${Buffer.from(`${options.clientId}:${options.clientSecret}`).toString(\"base64\")}`;\n const post = async (\n url: string,\n form: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<OAuthTokens> => {\n const response = await httpJson<TokenResponse>(url, {\n method: \"POST\",\n headers: { Authorization: basic },\n form,\n retries: 1,\n ...(signal ? { signal } : {}),\n });\n return toTokens(response);\n };\n return {\n authorizeUrl: ({ state, redirectUri }) => {\n const params = new URLSearchParams({\n client_id: options.clientId,\n redirect_uri: redirectUri,\n scope: SCOPE,\n state,\n response_type: \"code\",\n });\n return `${AUTHORIZE_URL}?${params.toString()}`;\n },\n exchangeCode: async ({ code, redirectUri, signal }) =>\n await post(\n TOKEN_URL,\n { redirect_uri: redirectUri, code, grant_type: \"authorization_code\" },\n signal,\n ),\n refresh: async ({ refreshToken, signal }) =>\n await post(REFRESH_URL, { refresh_token: refreshToken }, signal),\n };\n}\n\nfunction toTokens(response: TokenResponse): OAuthTokens {\n const seconds = response.expires_in ?? DEFAULT_EXPIRES_S;\n return {\n accessToken: response.access_token,\n refreshToken: response.refresh_token ?? null,\n expiresAt: new Date(Date.now() + seconds * 1000).toISOString(),\n };\n}\n"],"mappings":";;;;;AAUA,MAAM,gBAAgB;AACtB,MAAM,YAAY;AAClB,MAAM,cAAc;;AAEpB,MAAM,QAAQ;;AAEd,MAAM,oBAAoB;;AAS1B,SAAgB,kBACd,MAA0C,QAAQ,KACnB;CAC/B,MAAM,WAAW,IAAI;CACrB,MAAM,eAAe,IAAI;CACzB,OAAO,YAAY,eAAe;EAAE;EAAU;CAAa,IAAI,KAAA;AACjE;;AAGA,SAAgB,iBAAiB,SAAyC;CACxE,MAAM,QAAQ,SAAS,OAAO,KAAK,GAAG,QAAQ,SAAS,GAAG,QAAQ,cAAc,CAAC,CAAC,SAAS,QAAQ;CACnG,MAAM,OAAO,OACX,KACA,MACA,WACyB;EAQzB,OAAO,SAAS,MAPO,SAAwB,KAAK;GAClD,QAAQ;GACR,SAAS,EAAE,eAAe,MAAM;GAChC;GACA,SAAS;GACT,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,CAAC,CACuB;CAC1B;CACA,OAAO;EACL,eAAe,EAAE,OAAO,kBAAkB;GACxC,MAAM,SAAS,IAAI,gBAAgB;IACjC,WAAW,QAAQ;IACnB,cAAc;IACd,OAAO;IACP;IACA,eAAe;GACjB,CAAC;GACD,OAAO,GAAG,cAAc,GAAG,OAAO,SAAS;EAC7C;EACA,cAAc,OAAO,EAAE,MAAM,aAAa,aACxC,MAAM,KACJ,WACA;GAAE,cAAc;GAAa;GAAM,YAAY;EAAqB,GACpE,MACF;EACF,SAAS,OAAO,EAAE,cAAc,aAC9B,MAAM,KAAK,aAAa,EAAE,eAAe,aAAa,GAAG,MAAM;CACnE;AACF;AAEA,SAAS,SAAS,UAAsC;CACtD,MAAM,UAAU,SAAS,cAAc;CACvC,OAAO;EACL,aAAa,SAAS;EACtB,cAAc,SAAS,iBAAiB;EACxC,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,GAAI,CAAC,CAAC,YAAY;CAC/D;AACF"}
package/dist/url.d.mts ADDED
@@ -0,0 +1,13 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DesignRef } from "@storyshelf/core/adapter/design";
4
+ //#region src/url.d.ts
5
+ /**
6
+ * Parse a Figma file/design/board/proto/make URL, or null when it is not a Figma
7
+ * link. Embed (`/embed`), community and Figma Sites (`figma.site`) links are
8
+ * deliberately rejected, as are malformed `node-id` values.
9
+ */
10
+ declare function parseFigmaUrl(raw: string): DesignRef | null;
11
+ //#endregion
12
+ export { parseFigmaUrl };
13
+ //# sourceMappingURL=url.d.mts.map
package/dist/url.mjs ADDED
@@ -0,0 +1,43 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/url.ts
5
+ const FIGMA_HOSTS = /* @__PURE__ */ new Set(["figma.com", "www.figma.com"]);
6
+ const FILE_PATH = /^\/(?:file|design|proto|board|make)\/(?<key>[A-Za-z0-9]+)(?:\/|$)/u;
7
+ /** `1:2` after normalizing; Figma node ids are `<number>:<number>`. */
8
+ const NODE_ID = /^\d+:\d+$/u;
9
+ /**
10
+ * Parse a Figma file/design/board/proto/make URL, or null when it is not a Figma
11
+ * link. Embed (`/embed`), community and Figma Sites (`figma.site`) links are
12
+ * deliberately rejected, as are malformed `node-id` values.
13
+ */
14
+ function parseFigmaUrl(raw) {
15
+ let url;
16
+ try {
17
+ url = new URL(raw.trim());
18
+ } catch {
19
+ return null;
20
+ }
21
+ if (url.protocol !== "https:" || !FIGMA_HOSTS.has(url.hostname)) return null;
22
+ const fileKey = FILE_PATH.exec(url.pathname)?.groups?.["key"];
23
+ if (!fileKey) return null;
24
+ const node = url.searchParams.get("node-id")?.replaceAll("-", ":") ?? null;
25
+ if (node !== null && !NODE_ID.test(node)) return null;
26
+ return {
27
+ fileKey,
28
+ nodeId: node,
29
+ url: withoutCredentials(url)
30
+ };
31
+ }
32
+ /** The URL as stored and rendered into `href`s: no credentials, no fragment. */
33
+ function withoutCredentials(url) {
34
+ const clean = new URL(url);
35
+ clean.username = "";
36
+ clean.password = "";
37
+ clean.hash = "";
38
+ return clean.toString();
39
+ }
40
+ //#endregion
41
+ export { parseFigmaUrl };
42
+
43
+ //# sourceMappingURL=url.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.mjs","names":[],"sources":["../src/url.ts"],"sourcesContent":["/** Figma URL parsing: validates design links and extracts the file key and node id. */\nimport type { DesignRef } from \"@storyshelf/core/adapter/design\";\n\nconst FIGMA_HOSTS = new Set([\"figma.com\", \"www.figma.com\"]);\nconst FILE_PATH = /^\\/(?:file|design|proto|board|make)\\/(?<key>[A-Za-z0-9]+)(?:\\/|$)/u;\n/** `1:2` after normalizing; Figma node ids are `<number>:<number>`. */\nconst NODE_ID = /^\\d+:\\d+$/u;\n\n/**\n * Parse a Figma file/design/board/proto/make URL, or null when it is not a Figma\n * link. Embed (`/embed`), community and Figma Sites (`figma.site`) links are\n * deliberately rejected, as are malformed `node-id` values.\n */\nexport function parseFigmaUrl(raw: string): DesignRef | null {\n let url: URL;\n try {\n url = new URL(raw.trim());\n } catch {\n return null;\n }\n if (url.protocol !== \"https:\" || !FIGMA_HOSTS.has(url.hostname)) return null;\n const fileKey = FILE_PATH.exec(url.pathname)?.groups?.[\"key\"];\n if (!fileKey) return null;\n const node = url.searchParams.get(\"node-id\")?.replaceAll(\"-\", \":\") ?? null;\n if (node !== null && !NODE_ID.test(node)) return null;\n return { fileKey, nodeId: node, url: withoutCredentials(url) };\n}\n\n/** The URL as stored and rendered into `href`s: no credentials, no fragment. */\nfunction withoutCredentials(url: URL): string {\n const clean = new URL(url);\n clean.username = \"\";\n clean.password = \"\";\n clean.hash = \"\";\n return clean.toString();\n}\n"],"mappings":";;;;AAGA,MAAM,8BAAc,IAAI,IAAI,CAAC,aAAa,eAAe,CAAC;AAC1D,MAAM,YAAY;;AAElB,MAAM,UAAU;;;;;;AAOhB,SAAgB,cAAc,KAA+B;CAC3D,IAAI;CACJ,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,KAAK,CAAC;CAC1B,QAAQ;EACN,OAAO;CACT;CACA,IAAI,IAAI,aAAa,YAAY,CAAC,YAAY,IAAI,IAAI,QAAQ,GAAG,OAAO;CACxE,MAAM,UAAU,UAAU,KAAK,IAAI,QAAQ,CAAC,EAAE,SAAS;CACvD,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,OAAO,IAAI,aAAa,IAAI,SAAS,CAAC,EAAE,WAAW,KAAK,GAAG,KAAK;CACtE,IAAI,SAAS,QAAQ,CAAC,QAAQ,KAAK,IAAI,GAAG,OAAO;CACjD,OAAO;EAAE;EAAS,QAAQ;EAAM,KAAK,mBAAmB,GAAG;CAAE;AAC/D;;AAGA,SAAS,mBAAmB,KAAkB;CAC5C,MAAM,QAAQ,IAAI,IAAI,GAAG;CACzB,MAAM,WAAW;CACjB,MAAM,WAAW;CACjB,MAAM,OAAO;CACb,OAAO,MAAM,SAAS;AACxB"}
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@storyshelf/design-figma",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.7.0",
4
+ "description": "Figma design-link provider for StoryShelf: link validation, file metadata and rendered previews.",
5
+ "keywords": [
6
+ "design",
7
+ "figma",
8
+ "self-hosted",
9
+ "storybook"
10
+ ],
11
+ "homepage": "https://github.com/GuptaSiddhant/StoryShelf#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/GuptaSiddhant/StoryShelf/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": {
17
+ "name": "Siddhant Gupta",
18
+ "url": "https://guptasiddhant.com"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/GuptaSiddhant/StoryShelf.git",
23
+ "directory": "packages/design-figma"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "types": "./dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "source": "./src/index.ts",
34
+ "default": "./dist/index.mjs"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "publishConfig": {
39
+ "exports": {
40
+ ".": "./dist/index.mjs",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "dev": "tsdown -w",
48
+ "fmt": "oxfmt ./src",
49
+ "lint": "oxlint --type-aware --type-check ./src",
50
+ "test": "vitest run",
51
+ "prepublishOnly": "nub run build"
52
+ },
53
+ "dependencies": {
54
+ "@storyshelf/core": "0.7.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^26.0.0",
58
+ "@vitest/coverage-v8": "^4.0.0",
59
+ "oxfmt": "^0.65.0",
60
+ "oxlint": "^1.80.0",
61
+ "oxlint-tsgolint": "^7.0.2001",
62
+ "tsdown": "^0.22.14",
63
+ "typescript": "^7.0.2",
64
+ "vitest": "^5.0.0"
65
+ },
66
+ "engines": {
67
+ "node": ">=24"
68
+ },
69
+ "jsr": {
70
+ "runtimeCompat": {
71
+ "node": true,
72
+ "deno": true,
73
+ "browser": false,
74
+ "workerd": false,
75
+ "bun": true
76
+ }
77
+ }
78
+ }