@hirobius/design-system 0.8.1 → 0.10.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.
@@ -1,201 +0,0 @@
1
- /**
2
- * protocol/envelope.mjs
3
- *
4
- * Message-envelope protocol for bridge↔plugin traffic. Build unit p5-1.
5
- *
6
- * Envelope shape:
7
- * {
8
- * id: string (UUID v4)
9
- * type: string (verb, e.g. "selection.update")
10
- * payload: object
11
- * timestamp: number (ms since epoch)
12
- * hmac: string (HMAC-SHA256(secret, stableStringify({id,type,timestamp,payload})))
13
- * }
14
- *
15
- * The Figma plugin sandbox cannot import from this file. The mirror
16
- * implementation lives at `figma-agent-plugin/protocol-envelope.js` and
17
- * MUST stay byte-equivalent in HMAC output for the same inputs.
18
- *
19
- * Schema decisions (also documented in the unit's commit body):
20
- * - Clock-skew tolerance: ±60s on either side of now.
21
- * - Hard expiry: 5 minutes (300_000 ms) past `timestamp`.
22
- * - Replay-protection set: in-memory Map, FIFO-evicted at 1000 entries.
23
- *
24
- * Hard rules (CLAUDE.md / OPERATOR_BRIEF.md):
25
- * - No new npm dependencies; native Node crypto only.
26
- * - This file is the protocol library only — wiring the envelope into
27
- * hds-bridge.mjs / plugin code.js belongs to p5-2 and p5-3.
28
- */
29
-
30
- import crypto from 'node:crypto';
31
-
32
- export const CLOCK_SKEW_MS = 60_000; // ±60s tolerance for future timestamps
33
- export const MAX_AGE_MS = 5 * 60_000; // 5 min hard expiry
34
- export const REPLAY_SET_LIMIT = 1000; // FIFO eviction beyond this
35
-
36
- // ── stable serialization ───────────────────────────────────────────────────
37
-
38
- /**
39
- * Deterministic JSON serializer: object keys sorted lexicographically at
40
- * every level, no whitespace. Used as the HMAC pre-image so the Node-side
41
- * and plugin-side implementations produce identical signatures.
42
- */
43
- export function stableStringify(value) {
44
- if (value === null || typeof value !== 'object') {
45
- return JSON.stringify(value);
46
- }
47
- if (Array.isArray(value)) {
48
- return '[' + value.map((v) => stableStringify(v)).join(',') + ']';
49
- }
50
- const keys = Object.keys(value).sort();
51
- const parts = [];
52
- for (const k of keys) {
53
- const v = value[k];
54
- if (v === undefined) continue; // mirror JSON.stringify behavior
55
- parts.push(JSON.stringify(k) + ':' + stableStringify(v));
56
- }
57
- return '{' + parts.join(',') + '}';
58
- }
59
-
60
- // ── helpers ────────────────────────────────────────────────────────────────
61
-
62
- function computeHmac(secret, preimage) {
63
- return crypto.createHmac('sha256', String(secret)).update(preimage).digest('hex');
64
- }
65
-
66
- function preimageOf(envelope) {
67
- // HMAC binds the four schema fields. Order doesn't matter — stableStringify
68
- // sorts keys — but be explicit so the contract is readable.
69
- return stableStringify({
70
- id: envelope.id,
71
- payload: envelope.payload,
72
- timestamp: envelope.timestamp,
73
- type: envelope.type,
74
- });
75
- }
76
-
77
- function timingSafeEqualHex(a, b) {
78
- if (typeof a !== 'string' || typeof b !== 'string') return false;
79
- if (a.length !== b.length) return false;
80
- try {
81
- return crypto.timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
82
- } catch {
83
- return false;
84
- }
85
- }
86
-
87
- // ── replay-protection set (in-memory, module-scoped) ──────────────────────
88
-
89
- const _seenIds = new Map(); // id → timestamp; insertion-ordered for FIFO eviction
90
-
91
- function recordSeen(id) {
92
- if (_seenIds.has(id)) {
93
- // refresh insertion order is not desired — keep original entry
94
- return;
95
- }
96
- _seenIds.set(id, Date.now());
97
- while (_seenIds.size > REPLAY_SET_LIMIT) {
98
- const firstKey = _seenIds.keys().next().value;
99
- _seenIds.delete(firstKey);
100
- }
101
- }
102
-
103
- /** Test/diagnostics helper — clears the replay-protection set. */
104
- export function _resetReplaySet() {
105
- _seenIds.clear();
106
- }
107
-
108
- /** Test/diagnostics helper — current replay-set size. */
109
- export function _replaySetSize() {
110
- return _seenIds.size;
111
- }
112
-
113
- // ── public API ─────────────────────────────────────────────────────────────
114
-
115
- /**
116
- * Build a signed envelope.
117
- *
118
- * @param {string} type Verb, e.g. "selection.update".
119
- * @param {object} payload Arbitrary serializable object.
120
- * @param {string} secret Shared secret (UTF-8 string).
121
- * @returns {object} Envelope ready for transport.
122
- */
123
- export function createEnvelope(type, payload, secret) {
124
- if (typeof type !== 'string' || type.length === 0) {
125
- throw new TypeError('createEnvelope: type must be a non-empty string');
126
- }
127
- if (payload === null || typeof payload !== 'object') {
128
- throw new TypeError('createEnvelope: payload must be an object');
129
- }
130
- if (typeof secret !== 'string' || secret.length === 0) {
131
- throw new TypeError('createEnvelope: secret must be a non-empty string');
132
- }
133
- const envelope = {
134
- id: crypto.randomUUID(),
135
- type,
136
- payload,
137
- timestamp: Date.now(),
138
- };
139
- envelope.hmac = computeHmac(secret, preimageOf(envelope));
140
- return envelope;
141
- }
142
-
143
- /**
144
- * Verify a received envelope.
145
- *
146
- * @param {object} envelope
147
- * @param {string} secret
148
- * @param {{ now?: number }} [options] Optional clock override (testing).
149
- * @returns {{ ok: true } | { ok: false, code: string, message?: string }}
150
- */
151
- export function verifyEnvelope(envelope, secret, options = {}) {
152
- if (envelope === null || typeof envelope !== 'object') {
153
- return { ok: false, code: 'MALFORMED', message: 'envelope is not an object' };
154
- }
155
- const { id, type, payload, timestamp, hmac } = envelope;
156
- if (typeof id !== 'string' || id.length === 0) {
157
- return { ok: false, code: 'MALFORMED', message: 'missing id' };
158
- }
159
- if (typeof type !== 'string' || type.length === 0) {
160
- return { ok: false, code: 'MALFORMED', message: 'missing type' };
161
- }
162
- if (payload === null || typeof payload !== 'object') {
163
- return { ok: false, code: 'MALFORMED', message: 'missing or invalid payload' };
164
- }
165
- if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) {
166
- return { ok: false, code: 'MALFORMED', message: 'missing or invalid timestamp' };
167
- }
168
- if (typeof hmac !== 'string' || hmac.length === 0) {
169
- return { ok: false, code: 'MALFORMED', message: 'missing hmac' };
170
- }
171
- if (typeof secret !== 'string' || secret.length === 0) {
172
- return { ok: false, code: 'MALFORMED', message: 'verifier secret must be non-empty string' };
173
- }
174
-
175
- const now = typeof options.now === 'number' ? options.now : Date.now();
176
-
177
- // Future-skew check (timestamp claims to be too far in the future)
178
- if (timestamp - now > CLOCK_SKEW_MS) {
179
- return { ok: false, code: 'EXPIRED', message: 'timestamp is in the future beyond clock-skew tolerance' };
180
- }
181
- // Past expiry check (timestamp older than MAX_AGE_MS)
182
- if (now - timestamp > MAX_AGE_MS) {
183
- return { ok: false, code: 'EXPIRED', message: 'envelope older than max-age' };
184
- }
185
-
186
- // HMAC check
187
- const expected = computeHmac(secret, preimageOf(envelope));
188
- if (!timingSafeEqualHex(expected, hmac)) {
189
- return { ok: false, code: 'HMAC_MISMATCH', message: 'hmac signature does not match' };
190
- }
191
-
192
- // Replay check — must come after HMAC (don't pollute the set with junk)
193
- if (_seenIds.has(id)) {
194
- return { ok: false, code: 'REPLAY', message: 'envelope id already seen' };
195
- }
196
- recordSeen(id);
197
-
198
- return { ok: true };
199
- }
200
-
201
- export default { createEnvelope, verifyEnvelope, stableStringify };
@@ -1,190 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "figma-snapshot.schema.json",
4
- "title": "FigmaSnapshot",
5
- "description": "10f-1: Read-only normalized snapshot of a Figma document. Same field shapes as the p6-1 selection serializer, extended to cover the full document: all pages, variable collections/modes, all local components/styles.",
6
- "type": "object",
7
- "required": ["snapshotAt", "pages", "variables", "styles"],
8
- "additionalProperties": false,
9
- "properties": {
10
- "snapshotAt": {
11
- "type": "string",
12
- "format": "date-time",
13
- "description": "ISO-8601 timestamp (UTC) when the snapshot was taken by the plugin."
14
- },
15
- "documentName": {
16
- "type": "string",
17
- "description": "Figma document name (figma.root.name)."
18
- },
19
- "pages": {
20
- "type": "array",
21
- "description": "One entry per page in the document.",
22
- "items": { "$ref": "#/definitions/PageSummary" }
23
- },
24
- "variables": {
25
- "type": "object",
26
- "description": "Variable collections keyed by HDS collection name (HDS Primitive | HDS Semantic | HDS Component). Non-HDS collections are omitted.",
27
- "additionalProperties": { "$ref": "#/definitions/VariableCollection" }
28
- },
29
- "styles": {
30
- "type": "object",
31
- "description": "Local Figma styles grouped by kind.",
32
- "required": ["paint", "text", "effect", "grid"],
33
- "additionalProperties": false,
34
- "properties": {
35
- "paint": { "type": "array", "items": { "$ref": "#/definitions/StyleSummary" } },
36
- "text": { "type": "array", "items": { "$ref": "#/definitions/StyleSummary" } },
37
- "effect": { "type": "array", "items": { "$ref": "#/definitions/StyleSummary" } },
38
- "grid": { "type": "array", "items": { "$ref": "#/definitions/StyleSummary" } }
39
- }
40
- },
41
- "components": {
42
- "type": "array",
43
- "description": "All local COMPONENT and COMPONENT_SET nodes in the document (flattened list, not scoped per-page).",
44
- "items": { "$ref": "#/definitions/ComponentSummary" }
45
- }
46
- },
47
- "definitions": {
48
- "PageSummary": {
49
- "type": "object",
50
- "required": ["id", "name", "type"],
51
- "additionalProperties": false,
52
- "properties": {
53
- "id": { "type": "string" },
54
- "name": { "type": "string" },
55
- "type": { "type": "string", "const": "PAGE" },
56
- "frames": {
57
- "type": "array",
58
- "description": "Top-level FRAME and COMPONENT_SET children of this page, serialized with the p6-1 extractNodeTree shape (max depth 4).",
59
- "items": { "$ref": "#/definitions/SerializedNode" }
60
- }
61
- }
62
- },
63
- "VariableCollection": {
64
- "type": "object",
65
- "required": ["id", "name", "modes", "variables"],
66
- "additionalProperties": false,
67
- "properties": {
68
- "id": { "type": "string" },
69
- "name": { "type": "string" },
70
- "modes": {
71
- "type": "array",
72
- "items": {
73
- "type": "object",
74
- "required": ["modeId", "name"],
75
- "additionalProperties": false,
76
- "properties": {
77
- "modeId": { "type": "string" },
78
- "name": { "type": "string" }
79
- }
80
- }
81
- },
82
- "variables": {
83
- "type": "array",
84
- "items": { "$ref": "#/definitions/VariableSummary" }
85
- }
86
- }
87
- },
88
- "VariableSummary": {
89
- "type": "object",
90
- "required": ["id", "name", "resolvedType", "tokenPath"],
91
- "additionalProperties": false,
92
- "properties": {
93
- "id": { "type": "string" },
94
- "name": { "type": "string", "description": "Raw Figma variable name (slash/dash separated)." },
95
- "tokenPath": { "type": "string", "description": "Dot-notation canonical path, e.g. 'semantic.color.surface.raised'." },
96
- "resolvedType": { "type": "string", "enum": ["COLOR", "FLOAT", "STRING", "BOOLEAN"] },
97
- "valuesByMode": {
98
- "type": "object",
99
- "description": "Map of modeId → resolved value. COLOR values are serialized as hex strings (#rrggbb or #rrggbbaa). FLOAT/STRING/BOOLEAN are primitives.",
100
- "additionalProperties": {}
101
- },
102
- "scopes": {
103
- "type": "array",
104
- "items": { "type": "string" },
105
- "description": "Figma scopes (e.g. FILL_COLOR, CORNER_RADIUS). Omitted when empty."
106
- }
107
- }
108
- },
109
- "StyleSummary": {
110
- "type": "object",
111
- "required": ["id", "name", "type"],
112
- "additionalProperties": false,
113
- "properties": {
114
- "id": { "type": "string" },
115
- "name": { "type": "string" },
116
- "type": { "type": "string" },
117
- "description": { "type": "string" }
118
- }
119
- },
120
- "ComponentSummary": {
121
- "type": "object",
122
- "required": ["id", "name", "type"],
123
- "additionalProperties": false,
124
- "properties": {
125
- "id": { "type": "string" },
126
- "name": { "type": "string" },
127
- "type": { "type": "string", "enum": ["COMPONENT", "COMPONENT_SET"] },
128
- "componentName": {
129
- "type": "string",
130
- "description": "Resolved manifest spec key (from p6-1 resolveComponentName), if matched."
131
- },
132
- "variants": {
133
- "type": "array",
134
- "description": "Variant names for COMPONENT_SET nodes.",
135
- "items": { "type": "string" }
136
- },
137
- "properties": {
138
- "type": "object",
139
- "description": "Component property descriptors (Figma ComponentPropertyDefinitions).",
140
- "additionalProperties": {}
141
- },
142
- "pageId": { "type": "string", "description": "ID of the page that owns this component." },
143
- "a11y": { "$ref": "#/definitions/A11yMeta" }
144
- }
145
- },
146
- "SerializedNode": {
147
- "description": "p6-1 extractNodeTree shape, extended with slot bindings. Mirrors the selection serializer output exactly so downstream validators receive the same contract whether the input came from a selection or a full-page walk.",
148
- "type": "object",
149
- "required": ["id", "name", "type"],
150
- "additionalProperties": true,
151
- "properties": {
152
- "id": { "type": "string" },
153
- "name": { "type": "string" },
154
- "type": { "type": "string" },
155
- "width": { "type": "number" },
156
- "height": { "type": "number" },
157
- "x": { "type": "number" },
158
- "y": { "type": "number" },
159
- "fills": {},
160
- "boundVariables": {},
161
- "componentName": { "type": "string" },
162
- "tokenPaths": { "type": "object", "additionalProperties": {} },
163
- "a11y": { "$ref": "#/definitions/A11yMeta" },
164
- "slotBindings": { "$ref": "#/definitions/SlotBindings" },
165
- "children": {
166
- "type": "array",
167
- "items": { "$ref": "#/definitions/SerializedNode" }
168
- }
169
- }
170
- },
171
- "A11yMeta": {
172
- "type": "object",
173
- "additionalProperties": false,
174
- "properties": {
175
- "name": { "type": "string" },
176
- "role": { "type": "string" },
177
- "description": { "type": "string" }
178
- }
179
- },
180
- "SlotBindings": {
181
- "type": "object",
182
- "description": "Asset/portfolio slot assignments stored as plugin data on the node (hdsSlot key).",
183
- "additionalProperties": false,
184
- "properties": {
185
- "slot": { "type": "string", "description": "Slot identifier, e.g. 'hero-01'." },
186
- "assetId": { "type": "string" }
187
- }
188
- }
189
- }
190
- }