@payloadcms/figma 0.0.1-alpha.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.md +11 -0
- package/bin/cli.js +4 -0
- package/dist/api/control-plane.d.ts +124 -0
- package/dist/api/control-plane.d.ts.map +1 -0
- package/dist/api/control-plane.js +245 -0
- package/dist/api/control-plane.js.map +1 -0
- package/dist/api/control-plane.spec.d.ts +2 -0
- package/dist/api/control-plane.spec.d.ts.map +1 -0
- package/dist/api/control-plane.spec.js +296 -0
- package/dist/api/control-plane.spec.js.map +1 -0
- package/dist/api/figma-api.d.ts +31 -0
- package/dist/api/figma-api.d.ts.map +1 -0
- package/dist/api/figma-api.js +169 -0
- package/dist/api/figma-api.js.map +1 -0
- package/dist/api/figma-api.spec.d.ts +14 -0
- package/dist/api/figma-api.spec.d.ts.map +1 -0
- package/dist/api/figma-api.spec.js +201 -0
- package/dist/api/figma-api.spec.js.map +1 -0
- package/dist/auth/__tests__/fixtures.d.ts +26 -0
- package/dist/auth/__tests__/fixtures.d.ts.map +1 -0
- package/dist/auth/__tests__/fixtures.js +67 -0
- package/dist/auth/__tests__/fixtures.js.map +1 -0
- package/dist/auth/__tests__/mocks.d.ts +33 -0
- package/dist/auth/__tests__/mocks.d.ts.map +1 -0
- package/dist/auth/__tests__/mocks.js +63 -0
- package/dist/auth/__tests__/mocks.js.map +1 -0
- package/dist/auth/browser.d.ts +31 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +41 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/callback-server.d.ts +61 -0
- package/dist/auth/callback-server.d.ts.map +1 -0
- package/dist/auth/callback-server.js +168 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/callback-server.spec.d.ts +2 -0
- package/dist/auth/callback-server.spec.d.ts.map +1 -0
- package/dist/auth/callback-server.spec.js +85 -0
- package/dist/auth/callback-server.spec.js.map +1 -0
- package/dist/auth/crypto-utils.d.ts +33 -0
- package/dist/auth/crypto-utils.d.ts.map +1 -0
- package/dist/auth/crypto-utils.js +96 -0
- package/dist/auth/crypto-utils.js.map +1 -0
- package/dist/auth/crypto-utils.spec.d.ts +2 -0
- package/dist/auth/crypto-utils.spec.d.ts.map +1 -0
- package/dist/auth/crypto-utils.spec.js +36 -0
- package/dist/auth/crypto-utils.spec.js.map +1 -0
- package/dist/auth/jwt-validator.d.ts +41 -0
- package/dist/auth/jwt-validator.d.ts.map +1 -0
- package/dist/auth/jwt-validator.js +113 -0
- package/dist/auth/jwt-validator.js.map +1 -0
- package/dist/auth/jwt-validator.spec.d.ts +10 -0
- package/dist/auth/jwt-validator.spec.d.ts.map +1 -0
- package/dist/auth/jwt-validator.spec.js +236 -0
- package/dist/auth/jwt-validator.spec.js.map +1 -0
- package/dist/auth/oauth-flow.d.ts +62 -0
- package/dist/auth/oauth-flow.d.ts.map +1 -0
- package/dist/auth/oauth-flow.js +172 -0
- package/dist/auth/oauth-flow.js.map +1 -0
- package/dist/auth/oauth-flow.spec.d.ts +2 -0
- package/dist/auth/oauth-flow.spec.d.ts.map +1 -0
- package/dist/auth/oauth-flow.spec.js +134 -0
- package/dist/auth/oauth-flow.spec.js.map +1 -0
- package/dist/auth/pages/error.html +85 -0
- package/dist/auth/pages/success.html +74 -0
- package/dist/auth/pkce.d.ts +51 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +71 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/pkce.spec.d.ts +2 -0
- package/dist/auth/pkce.spec.d.ts.map +1 -0
- package/dist/auth/pkce.spec.js +32 -0
- package/dist/auth/pkce.spec.js.map +1 -0
- package/dist/auth/project-token.d.ts +47 -0
- package/dist/auth/project-token.d.ts.map +1 -0
- package/dist/auth/project-token.js +134 -0
- package/dist/auth/project-token.js.map +1 -0
- package/dist/auth/project-token.spec.d.ts +2 -0
- package/dist/auth/project-token.spec.d.ts.map +1 -0
- package/dist/auth/project-token.spec.js +332 -0
- package/dist/auth/project-token.spec.js.map +1 -0
- package/dist/auth/refresh.d.ts +35 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +149 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/auth/refresh.spec.d.ts +2 -0
- package/dist/auth/refresh.spec.d.ts.map +1 -0
- package/dist/auth/refresh.spec.js +105 -0
- package/dist/auth/refresh.spec.js.map +1 -0
- package/dist/auth/token-store.d.ts +110 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +188 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/token-store.spec.d.ts +2 -0
- package/dist/auth/token-store.spec.d.ts.map +1 -0
- package/dist/auth/token-store.spec.js +276 -0
- package/dist/auth/token-store.spec.js.map +1 -0
- package/dist/auth/types.d.ts +197 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +6 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +108 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/__tests__/test-utils.d.ts +28 -0
- package/dist/commands/__tests__/test-utils.d.ts.map +1 -0
- package/dist/commands/__tests__/test-utils.js +34 -0
- package/dist/commands/__tests__/test-utils.js.map +1 -0
- package/dist/commands/deploy.d.ts +16 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +297 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/deploy.spec.d.ts +2 -0
- package/dist/commands/deploy.spec.d.ts.map +1 -0
- package/dist/commands/deploy.spec.js +18 -0
- package/dist/commands/deploy.spec.js.map +1 -0
- package/dist/commands/init.d.ts +30 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +410 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.spec.d.ts +2 -0
- package/dist/commands/init.spec.d.ts.map +1 -0
- package/dist/commands/init.spec.js +283 -0
- package/dist/commands/init.spec.js.map +1 -0
- package/dist/commands/list-tokens.d.ts +16 -0
- package/dist/commands/list-tokens.d.ts.map +1 -0
- package/dist/commands/list-tokens.js +44 -0
- package/dist/commands/list-tokens.js.map +1 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +69 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +16 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +43 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/config/oauth.d.ts +19 -0
- package/dist/config/oauth.d.ts.map +1 -0
- package/dist/config/oauth.js +43 -0
- package/dist/config/oauth.js.map +1 -0
- package/dist/constants.d.ts +27 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +27 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/adapter.d.ts +5 -0
- package/dist/plugin/adapter.d.ts.map +1 -0
- package/dist/plugin/adapter.js +139 -0
- package/dist/plugin/adapter.js.map +1 -0
- package/dist/plugin/build-config.d.ts +37 -0
- package/dist/plugin/build-config.d.ts.map +1 -0
- package/dist/plugin/build-config.js +25 -0
- package/dist/plugin/build-config.js.map +1 -0
- package/dist/plugin/build-config.spec.d.ts +2 -0
- package/dist/plugin/build-config.spec.d.ts.map +1 -0
- package/dist/plugin/build-config.spec.js +72 -0
- package/dist/plugin/build-config.spec.js.map +1 -0
- package/dist/types/config.d.ts +45 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +9 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/asset-collection.d.ts +30 -0
- package/dist/utils/asset-collection.d.ts.map +1 -0
- package/dist/utils/asset-collection.js +117 -0
- package/dist/utils/asset-collection.js.map +1 -0
- package/dist/utils/asset-collection.spec.d.ts +2 -0
- package/dist/utils/asset-collection.spec.d.ts.map +1 -0
- package/dist/utils/asset-collection.spec.js +155 -0
- package/dist/utils/asset-collection.spec.js.map +1 -0
- package/dist/utils/build-detection.d.ts +32 -0
- package/dist/utils/build-detection.d.ts.map +1 -0
- package/dist/utils/build-detection.js +55 -0
- package/dist/utils/build-detection.js.map +1 -0
- package/dist/utils/build-detection.spec.d.ts +2 -0
- package/dist/utils/build-detection.spec.d.ts.map +1 -0
- package/dist/utils/build-detection.spec.js +110 -0
- package/dist/utils/build-detection.spec.js.map +1 -0
- package/dist/utils/config.d.ts +33 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +78 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/config.spec.d.ts +2 -0
- package/dist/utils/config.spec.d.ts.map +1 -0
- package/dist/utils/config.spec.js +167 -0
- package/dist/utils/config.spec.js.map +1 -0
- package/dist/utils/download-template.d.ts +15 -0
- package/dist/utils/download-template.d.ts.map +1 -0
- package/dist/utils/download-template.js +74 -0
- package/dist/utils/download-template.js.map +1 -0
- package/dist/utils/download-template.spec.d.ts +2 -0
- package/dist/utils/download-template.spec.d.ts.map +1 -0
- package/dist/utils/download-template.spec.js +76 -0
- package/dist/utils/download-template.spec.js.map +1 -0
- package/dist/utils/env-management.d.ts +16 -0
- package/dist/utils/env-management.d.ts.map +1 -0
- package/dist/utils/env-management.js +98 -0
- package/dist/utils/env-management.js.map +1 -0
- package/dist/utils/env-management.spec.d.ts +2 -0
- package/dist/utils/env-management.spec.d.ts.map +1 -0
- package/dist/utils/env-management.spec.js +123 -0
- package/dist/utils/env-management.spec.js.map +1 -0
- package/dist/utils/format.d.ts +23 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +49 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/formatter.d.ts +11 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +47 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/git.d.ts +15 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +92 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/git.spec.d.ts +2 -0
- package/dist/utils/git.spec.d.ts.map +1 -0
- package/dist/utils/git.spec.js +99 -0
- package/dist/utils/git.spec.js.map +1 -0
- package/dist/utils/is-debug.d.ts +6 -0
- package/dist/utils/is-debug.d.ts.map +1 -0
- package/dist/utils/is-debug.js +8 -0
- package/dist/utils/is-debug.js.map +1 -0
- package/dist/utils/lambda-config.d.ts +7 -0
- package/dist/utils/lambda-config.d.ts.map +1 -0
- package/dist/utils/lambda-config.js +144 -0
- package/dist/utils/lambda-config.js.map +1 -0
- package/dist/utils/lambda-config.spec.d.ts +2 -0
- package/dist/utils/lambda-config.spec.d.ts.map +1 -0
- package/dist/utils/lambda-config.spec.js +141 -0
- package/dist/utils/lambda-config.spec.js.map +1 -0
- package/dist/utils/log.d.ts +6 -0
- package/dist/utils/log.d.ts.map +1 -0
- package/dist/utils/log.js +22 -0
- package/dist/utils/log.js.map +1 -0
- package/dist/utils/messages.d.ts +10 -0
- package/dist/utils/messages.d.ts.map +1 -0
- package/dist/utils/messages.js +96 -0
- package/dist/utils/messages.js.map +1 -0
- package/dist/utils/package-manager.d.ts +12 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +37 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/payload-config-ast.d.ts +30 -0
- package/dist/utils/payload-config-ast.d.ts.map +1 -0
- package/dist/utils/payload-config-ast.js +259 -0
- package/dist/utils/payload-config-ast.js.map +1 -0
- package/dist/utils/payload-config-ast.spec.d.ts +2 -0
- package/dist/utils/payload-config-ast.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-ast.spec.js +303 -0
- package/dist/utils/payload-config-ast.spec.js.map +1 -0
- package/dist/utils/payload-config-finder.d.ts +9 -0
- package/dist/utils/payload-config-finder.d.ts.map +1 -0
- package/dist/utils/payload-config-finder.js +26 -0
- package/dist/utils/payload-config-finder.js.map +1 -0
- package/dist/utils/payload-config-modifier.d.ts +18 -0
- package/dist/utils/payload-config-modifier.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.int.spec.d.ts +2 -0
- package/dist/utils/payload-config-modifier.int.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.int.spec.js +116 -0
- package/dist/utils/payload-config-modifier.int.spec.js.map +1 -0
- package/dist/utils/payload-config-modifier.js +232 -0
- package/dist/utils/payload-config-modifier.js.map +1 -0
- package/dist/utils/payload-config-modifier.spec.d.ts +2 -0
- package/dist/utils/payload-config-modifier.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.spec.js +162 -0
- package/dist/utils/payload-config-modifier.spec.js.map +1 -0
- package/dist/utils/payload-package-check.d.ts +15 -0
- package/dist/utils/payload-package-check.d.ts.map +1 -0
- package/dist/utils/payload-package-check.js +116 -0
- package/dist/utils/payload-package-check.js.map +1 -0
- package/dist/utils/payload-package-check.spec.d.ts +2 -0
- package/dist/utils/payload-package-check.spec.d.ts.map +1 -0
- package/dist/utils/payload-package-check.spec.js +148 -0
- package/dist/utils/payload-package-check.spec.js.map +1 -0
- package/dist/utils/project.d.ts +58 -0
- package/dist/utils/project.d.ts.map +1 -0
- package/dist/utils/project.js +252 -0
- package/dist/utils/project.js.map +1 -0
- package/dist/utils/project.spec.d.ts +2 -0
- package/dist/utils/project.spec.d.ts.map +1 -0
- package/dist/utils/project.spec.js +222 -0
- package/dist/utils/project.spec.js.map +1 -0
- package/dist/utils/s3-upload.d.ts +43 -0
- package/dist/utils/s3-upload.d.ts.map +1 -0
- package/dist/utils/s3-upload.js +128 -0
- package/dist/utils/s3-upload.js.map +1 -0
- package/dist/utils/s3-upload.spec.d.ts +2 -0
- package/dist/utils/s3-upload.spec.d.ts.map +1 -0
- package/dist/utils/s3-upload.spec.js +140 -0
- package/dist/utils/s3-upload.spec.js.map +1 -0
- package/dist/utils/token-display.d.ts +14 -0
- package/dist/utils/token-display.d.ts.map +1 -0
- package/dist/utils/token-display.js +62 -0
- package/dist/utils/token-display.js.map +1 -0
- package/dist/utils/typescript-validator.d.ts +11 -0
- package/dist/utils/typescript-validator.d.ts.map +1 -0
- package/dist/utils/typescript-validator.js +58 -0
- package/dist/utils/typescript-validator.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createDeployment, createTenant, getTenantDetails, listTenants, performDeployment } from './control-plane.js';
|
|
3
|
+
describe('control-plane API (mocked)', ()=>{
|
|
4
|
+
beforeEach(()=>{
|
|
5
|
+
vi.clearAllMocks();
|
|
6
|
+
});
|
|
7
|
+
afterEach(()=>{
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
describe('listTenants', ()=>{
|
|
11
|
+
it('should return array of tenant instances', async ()=>{
|
|
12
|
+
const tenants = await listTenants('mock-access-token');
|
|
13
|
+
expect(Array.isArray(tenants)).toBe(true);
|
|
14
|
+
expect(tenants.length).toBeGreaterThan(0);
|
|
15
|
+
});
|
|
16
|
+
it('should return tenants with required fields', async ()=>{
|
|
17
|
+
const tenants = await listTenants('mock-access-token');
|
|
18
|
+
tenants.forEach((tenant)=>{
|
|
19
|
+
expect(tenant).toHaveProperty('id');
|
|
20
|
+
expect(tenant).toHaveProperty('domain');
|
|
21
|
+
expect(tenant).toHaveProperty('status');
|
|
22
|
+
expect(tenant).toHaveProperty('parentType');
|
|
23
|
+
expect(tenant).toHaveProperty('parentId');
|
|
24
|
+
expect(tenant).toHaveProperty('createdAt');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('should return tenants with valid status', async ()=>{
|
|
28
|
+
const tenants = await listTenants('mock-access-token');
|
|
29
|
+
tenants.forEach((tenant)=>{
|
|
30
|
+
expect([
|
|
31
|
+
'active',
|
|
32
|
+
'provisioning',
|
|
33
|
+
'inactive'
|
|
34
|
+
]).toContain(tenant.status);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
it('should return tenants with valid parentType', async ()=>{
|
|
38
|
+
const tenants = await listTenants('mock-access-token');
|
|
39
|
+
tenants.forEach((tenant)=>{
|
|
40
|
+
expect([
|
|
41
|
+
'team',
|
|
42
|
+
'org',
|
|
43
|
+
'workspace'
|
|
44
|
+
]).toContain(tenant.parentType);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('should handle different access tokens', async ()=>{
|
|
48
|
+
const tenants1 = await listTenants('token-1');
|
|
49
|
+
const tenants2 = await listTenants('token-2');
|
|
50
|
+
// Mock should return same structure regardless of token
|
|
51
|
+
expect(tenants1.length).toBe(tenants2.length);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('createTenant', ()=>{
|
|
55
|
+
it('should create tenant with name', async ()=>{
|
|
56
|
+
const tenant = await createTenant('mock-access-token', {
|
|
57
|
+
name: 'test-cms'
|
|
58
|
+
});
|
|
59
|
+
expect(tenant).toHaveProperty('id');
|
|
60
|
+
expect(tenant).toHaveProperty('domain');
|
|
61
|
+
expect(tenant.domain).toContain('test-cms');
|
|
62
|
+
});
|
|
63
|
+
it('should return tenant with provisioning status', async ()=>{
|
|
64
|
+
const tenant = await createTenant('mock-access-token', {
|
|
65
|
+
name: 'new-cms'
|
|
66
|
+
});
|
|
67
|
+
expect(tenant.status).toBe('provisioning');
|
|
68
|
+
});
|
|
69
|
+
it('should generate unique tenant IDs', async ()=>{
|
|
70
|
+
const tenant1 = await createTenant('mock-access-token', {
|
|
71
|
+
name: 'cms-1'
|
|
72
|
+
});
|
|
73
|
+
const tenant2 = await createTenant('mock-access-token', {
|
|
74
|
+
name: 'cms-2'
|
|
75
|
+
});
|
|
76
|
+
expect(tenant1.id).not.toBe(tenant2.id);
|
|
77
|
+
});
|
|
78
|
+
it('should handle names with special characters', async ()=>{
|
|
79
|
+
const tenant = await createTenant('mock-access-token', {
|
|
80
|
+
name: 'My Test CMS 123!'
|
|
81
|
+
});
|
|
82
|
+
// Should convert to slug format
|
|
83
|
+
expect(tenant.domain).toMatch(/^[a-z0-9-]+\.figmacms\.com$/);
|
|
84
|
+
});
|
|
85
|
+
it('should include optional parent fields', async ()=>{
|
|
86
|
+
const tenant = await createTenant('mock-access-token', {
|
|
87
|
+
name: 'test-cms',
|
|
88
|
+
parentId: 'org_custom',
|
|
89
|
+
parentType: 'org'
|
|
90
|
+
});
|
|
91
|
+
expect(tenant.parentType).toBe('org');
|
|
92
|
+
expect(tenant.parentId).toBe('org_custom');
|
|
93
|
+
});
|
|
94
|
+
it('should default to team parent type', async ()=>{
|
|
95
|
+
const tenant = await createTenant('mock-access-token', {
|
|
96
|
+
name: 'test-cms'
|
|
97
|
+
});
|
|
98
|
+
expect(tenant.parentType).toBe('team');
|
|
99
|
+
});
|
|
100
|
+
it('should include createdAt timestamp', async ()=>{
|
|
101
|
+
const tenant = await createTenant('mock-access-token', {
|
|
102
|
+
name: 'test-cms'
|
|
103
|
+
});
|
|
104
|
+
expect(tenant.createdAt).toBeDefined();
|
|
105
|
+
expect(new Date(tenant.createdAt).getTime()).toBeGreaterThan(0);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('getTenantDetails', ()=>{
|
|
109
|
+
it('should return tenant details for given ID', async ()=>{
|
|
110
|
+
const tenant = await getTenantDetails('mock-access-token', 'tenant_abc123');
|
|
111
|
+
expect(tenant).toHaveProperty('id');
|
|
112
|
+
expect(tenant.id).toBe('tenant_abc123');
|
|
113
|
+
});
|
|
114
|
+
it('should return tenant with all required fields', async ()=>{
|
|
115
|
+
const tenant = await getTenantDetails('mock-access-token', 'tenant_test');
|
|
116
|
+
expect(tenant).toHaveProperty('id');
|
|
117
|
+
expect(tenant).toHaveProperty('domain');
|
|
118
|
+
expect(tenant).toHaveProperty('status');
|
|
119
|
+
expect(tenant).toHaveProperty('parentType');
|
|
120
|
+
expect(tenant).toHaveProperty('parentId');
|
|
121
|
+
expect(tenant).toHaveProperty('createdAt');
|
|
122
|
+
});
|
|
123
|
+
it('should return active status for mock tenant', async ()=>{
|
|
124
|
+
const tenant = await getTenantDetails('mock-access-token', 'tenant_test');
|
|
125
|
+
expect(tenant.status).toBe('active');
|
|
126
|
+
});
|
|
127
|
+
it('should handle different tenant IDs', async ()=>{
|
|
128
|
+
const tenant1 = await getTenantDetails('mock-access-token', 'tenant_1');
|
|
129
|
+
const tenant2 = await getTenantDetails('mock-access-token', 'tenant_2');
|
|
130
|
+
expect(tenant1.id).toBe('tenant_1');
|
|
131
|
+
expect(tenant2.id).toBe('tenant_2');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('createDeployment', ()=>{
|
|
135
|
+
it('should return deployment ID and upload URLs', async ()=>{
|
|
136
|
+
const result = await createDeployment('mock-token', 'tenant_123', {
|
|
137
|
+
staticAssets: [
|
|
138
|
+
'_next/static/abc.js',
|
|
139
|
+
'_next/static/xyz.css'
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
expect(result).toHaveProperty('deploymentId');
|
|
143
|
+
expect(result).toHaveProperty('codeUploadUrl');
|
|
144
|
+
expect(result).toHaveProperty('staticAssetUploadUrls');
|
|
145
|
+
expect(result.deploymentId).toMatch(/^deploy_/);
|
|
146
|
+
});
|
|
147
|
+
it('should include upload URL for each static asset', async ()=>{
|
|
148
|
+
const result = await createDeployment('mock-token', 'tenant_456', {
|
|
149
|
+
staticAssets: [
|
|
150
|
+
'_next/static/abc.js',
|
|
151
|
+
'_next/static/xyz.css'
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
expect(result.staticAssetUploadUrls).toHaveProperty('_next/static/abc.js');
|
|
155
|
+
expect(result.staticAssetUploadUrls).toHaveProperty('_next/static/xyz.css');
|
|
156
|
+
expect(result.staticAssetUploadUrls['_next/static/abc.js']).toContain('https://');
|
|
157
|
+
});
|
|
158
|
+
it('should return S3 upload URL format', async ()=>{
|
|
159
|
+
const result = await createDeployment('mock-token', 'tenant_789', {
|
|
160
|
+
staticAssets: [
|
|
161
|
+
'_next/static/test.js'
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
expect(result.codeUploadUrl).toMatch(/^https:\/\/.*\.s3\..*\.amazonaws\.com\//);
|
|
165
|
+
expect(result.staticAssetUploadUrls['_next/static/test.js']).toMatch(/^https:\/\/.*\.s3\..*\.amazonaws\.com\//);
|
|
166
|
+
});
|
|
167
|
+
it('should handle empty static assets array', async ()=>{
|
|
168
|
+
const result = await createDeployment('mock-token', 'tenant_empty', {
|
|
169
|
+
staticAssets: []
|
|
170
|
+
});
|
|
171
|
+
expect(result.deploymentId).toBeDefined();
|
|
172
|
+
expect(result.codeUploadUrl).toBeDefined();
|
|
173
|
+
expect(Object.keys(result.staticAssetUploadUrls)).toHaveLength(0);
|
|
174
|
+
});
|
|
175
|
+
it('should generate unique deployment IDs', async ()=>{
|
|
176
|
+
const result1 = await createDeployment('mock-token', 'tenant_abc', {
|
|
177
|
+
staticAssets: []
|
|
178
|
+
});
|
|
179
|
+
const result2 = await createDeployment('mock-token', 'tenant_abc', {
|
|
180
|
+
staticAssets: []
|
|
181
|
+
});
|
|
182
|
+
expect(result1.deploymentId).not.toBe(result2.deploymentId);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('performDeployment', ()=>{
|
|
186
|
+
it('should deploy using deployment ID', async ()=>{
|
|
187
|
+
const result = await performDeployment('mock-token', 'tenant_123', {
|
|
188
|
+
deploymentId: 'deploy_abc123'
|
|
189
|
+
});
|
|
190
|
+
expect(result).toHaveProperty('deploymentId');
|
|
191
|
+
expect(result).toHaveProperty('message');
|
|
192
|
+
expect(result.deploymentId).toBe('deploy_abc123');
|
|
193
|
+
});
|
|
194
|
+
it('should return success message', async ()=>{
|
|
195
|
+
const result = await performDeployment('mock-token', 'tenant_456', {
|
|
196
|
+
deploymentId: 'deploy_xyz789'
|
|
197
|
+
});
|
|
198
|
+
expect(result.message).toBeDefined();
|
|
199
|
+
expect(result.message).toBe('Deployment initiated successfully');
|
|
200
|
+
expect(result.message.length).toBeGreaterThan(0);
|
|
201
|
+
});
|
|
202
|
+
it('should handle different tenant IDs', async ()=>{
|
|
203
|
+
const result1 = await performDeployment('mock-token', 'tenant_aaa', {
|
|
204
|
+
deploymentId: 'deploy_1'
|
|
205
|
+
});
|
|
206
|
+
const result2 = await performDeployment('mock-token', 'tenant_bbb', {
|
|
207
|
+
deploymentId: 'deploy_2'
|
|
208
|
+
});
|
|
209
|
+
expect(result1.deploymentId).toBe('deploy_1');
|
|
210
|
+
expect(result2.deploymentId).toBe('deploy_2');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('error handling', ()=>{
|
|
214
|
+
beforeEach(()=>{
|
|
215
|
+
// Force real API mode for error testing
|
|
216
|
+
process.env.FIGMA_MOCK_CONTROL_PLANE = 'false';
|
|
217
|
+
});
|
|
218
|
+
afterEach(()=>{
|
|
219
|
+
// Restore mock mode
|
|
220
|
+
delete process.env.FIGMA_MOCK_CONTROL_PLANE;
|
|
221
|
+
});
|
|
222
|
+
it('createDeployment should throw ControlPlaneError on API failure', async ()=>{
|
|
223
|
+
// Mock fetch to return error
|
|
224
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
225
|
+
ok: false,
|
|
226
|
+
status: 500,
|
|
227
|
+
statusText: 'Internal Server Error'
|
|
228
|
+
});
|
|
229
|
+
await expect(createDeployment('bad-token', 'tenant_fail', {
|
|
230
|
+
staticAssets: []
|
|
231
|
+
})).rejects.toThrow('Failed to create deployment: 500 Internal Server Error');
|
|
232
|
+
});
|
|
233
|
+
it('performDeployment should throw ControlPlaneError on API failure', async ()=>{
|
|
234
|
+
// Mock fetch to return error
|
|
235
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
236
|
+
ok: false,
|
|
237
|
+
status: 404,
|
|
238
|
+
statusText: 'Not Found'
|
|
239
|
+
});
|
|
240
|
+
await expect(performDeployment('bad-token', 'tenant_missing', {
|
|
241
|
+
deploymentId: 'deploy_notfound'
|
|
242
|
+
})).rejects.toThrow('Failed to perform deployment: 404 Not Found');
|
|
243
|
+
});
|
|
244
|
+
it('createDeployment should map snake_case API response to camelCase', async ()=>{
|
|
245
|
+
// Mock fetch to return successful API response with snake_case
|
|
246
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
247
|
+
json: ()=>({
|
|
248
|
+
error: false,
|
|
249
|
+
meta: {
|
|
250
|
+
code_upload_url: 'https://s3.amazonaws.com/code/upload',
|
|
251
|
+
deployment_id: 'deploy_test123',
|
|
252
|
+
static_asset_upload_urls: {
|
|
253
|
+
'_next/static/test.js': 'https://s3.amazonaws.com/static/test.js'
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
status: 200
|
|
257
|
+
}),
|
|
258
|
+
ok: true
|
|
259
|
+
});
|
|
260
|
+
const result = await createDeployment('token', 'tenant_123', {
|
|
261
|
+
staticAssets: [
|
|
262
|
+
'_next/static/test.js'
|
|
263
|
+
]
|
|
264
|
+
});
|
|
265
|
+
// Verify response is mapped to camelCase
|
|
266
|
+
expect(result.deploymentId).toBe('deploy_test123');
|
|
267
|
+
expect(result.codeUploadUrl).toBe('https://s3.amazonaws.com/code/upload');
|
|
268
|
+
expect(result.staticAssetUploadUrls).toEqual({
|
|
269
|
+
'_next/static/test.js': 'https://s3.amazonaws.com/static/test.js'
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
it('performDeployment should map snake_case API response to camelCase', async ()=>{
|
|
273
|
+
// Mock fetch to return successful API response with snake_case
|
|
274
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
275
|
+
json: ()=>({
|
|
276
|
+
error: false,
|
|
277
|
+
meta: {
|
|
278
|
+
deployment_id: 'deploy_abc456',
|
|
279
|
+
message: 'Deployment successful',
|
|
280
|
+
status: 'deployed'
|
|
281
|
+
},
|
|
282
|
+
status: 200
|
|
283
|
+
}),
|
|
284
|
+
ok: true
|
|
285
|
+
});
|
|
286
|
+
const result = await performDeployment('token', 'tenant_456', {
|
|
287
|
+
deploymentId: 'deploy_abc456'
|
|
288
|
+
});
|
|
289
|
+
// Verify response is mapped to camelCase
|
|
290
|
+
expect(result.deploymentId).toBe('deploy_abc456');
|
|
291
|
+
expect(result.message).toBe('Deployment successful');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
//# sourceMappingURL=control-plane.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/control-plane.spec.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport {\n createDeployment,\n createTenant,\n getTenantDetails,\n listTenants,\n performDeployment,\n} from './control-plane.js'\n\ndescribe('control-plane API (mocked)', () => {\n beforeEach(() => {\n vi.clearAllMocks()\n })\n\n afterEach(() => {\n vi.restoreAllMocks()\n })\n\n describe('listTenants', () => {\n it('should return array of tenant instances', async () => {\n const tenants = await listTenants('mock-access-token')\n\n expect(Array.isArray(tenants)).toBe(true)\n expect(tenants.length).toBeGreaterThan(0)\n })\n\n it('should return tenants with required fields', async () => {\n const tenants = await listTenants('mock-access-token')\n\n tenants.forEach((tenant) => {\n expect(tenant).toHaveProperty('id')\n expect(tenant).toHaveProperty('domain')\n expect(tenant).toHaveProperty('status')\n expect(tenant).toHaveProperty('parentType')\n expect(tenant).toHaveProperty('parentId')\n expect(tenant).toHaveProperty('createdAt')\n })\n })\n\n it('should return tenants with valid status', async () => {\n const tenants = await listTenants('mock-access-token')\n\n tenants.forEach((tenant) => {\n expect(['active', 'provisioning', 'inactive']).toContain(tenant.status)\n })\n })\n\n it('should return tenants with valid parentType', async () => {\n const tenants = await listTenants('mock-access-token')\n\n tenants.forEach((tenant) => {\n expect(['team', 'org', 'workspace']).toContain(tenant.parentType)\n })\n })\n\n it('should handle different access tokens', async () => {\n const tenants1 = await listTenants('token-1')\n const tenants2 = await listTenants('token-2')\n\n // Mock should return same structure regardless of token\n expect(tenants1.length).toBe(tenants2.length)\n })\n })\n\n describe('createTenant', () => {\n it('should create tenant with name', async () => {\n const tenant = await createTenant('mock-access-token', {\n name: 'test-cms',\n })\n\n expect(tenant).toHaveProperty('id')\n expect(tenant).toHaveProperty('domain')\n expect(tenant.domain).toContain('test-cms')\n })\n\n it('should return tenant with provisioning status', async () => {\n const tenant = await createTenant('mock-access-token', {\n name: 'new-cms',\n })\n\n expect(tenant.status).toBe('provisioning')\n })\n\n it('should generate unique tenant IDs', async () => {\n const tenant1 = await createTenant('mock-access-token', {\n name: 'cms-1',\n })\n const tenant2 = await createTenant('mock-access-token', {\n name: 'cms-2',\n })\n\n expect(tenant1.id).not.toBe(tenant2.id)\n })\n\n it('should handle names with special characters', async () => {\n const tenant = await createTenant('mock-access-token', {\n name: 'My Test CMS 123!',\n })\n\n // Should convert to slug format\n expect(tenant.domain).toMatch(/^[a-z0-9-]+\\.figmacms\\.com$/)\n })\n\n it('should include optional parent fields', async () => {\n const tenant = await createTenant('mock-access-token', {\n name: 'test-cms',\n parentId: 'org_custom',\n parentType: 'org',\n })\n\n expect(tenant.parentType).toBe('org')\n expect(tenant.parentId).toBe('org_custom')\n })\n\n it('should default to team parent type', async () => {\n const tenant = await createTenant('mock-access-token', {\n name: 'test-cms',\n })\n\n expect(tenant.parentType).toBe('team')\n })\n\n it('should include createdAt timestamp', async () => {\n const tenant = await createTenant('mock-access-token', {\n name: 'test-cms',\n })\n\n expect(tenant.createdAt).toBeDefined()\n expect(new Date(tenant.createdAt!).getTime()).toBeGreaterThan(0)\n })\n })\n\n describe('getTenantDetails', () => {\n it('should return tenant details for given ID', async () => {\n const tenant = await getTenantDetails('mock-access-token', 'tenant_abc123')\n\n expect(tenant).toHaveProperty('id')\n expect(tenant.id).toBe('tenant_abc123')\n })\n\n it('should return tenant with all required fields', async () => {\n const tenant = await getTenantDetails('mock-access-token', 'tenant_test')\n\n expect(tenant).toHaveProperty('id')\n expect(tenant).toHaveProperty('domain')\n expect(tenant).toHaveProperty('status')\n expect(tenant).toHaveProperty('parentType')\n expect(tenant).toHaveProperty('parentId')\n expect(tenant).toHaveProperty('createdAt')\n })\n\n it('should return active status for mock tenant', async () => {\n const tenant = await getTenantDetails('mock-access-token', 'tenant_test')\n\n expect(tenant.status).toBe('active')\n })\n\n it('should handle different tenant IDs', async () => {\n const tenant1 = await getTenantDetails('mock-access-token', 'tenant_1')\n const tenant2 = await getTenantDetails('mock-access-token', 'tenant_2')\n\n expect(tenant1.id).toBe('tenant_1')\n expect(tenant2.id).toBe('tenant_2')\n })\n })\n\n describe('createDeployment', () => {\n it('should return deployment ID and upload URLs', async () => {\n const result = await createDeployment('mock-token', 'tenant_123', {\n staticAssets: ['_next/static/abc.js', '_next/static/xyz.css'],\n })\n\n expect(result).toHaveProperty('deploymentId')\n expect(result).toHaveProperty('codeUploadUrl')\n expect(result).toHaveProperty('staticAssetUploadUrls')\n expect(result.deploymentId).toMatch(/^deploy_/)\n })\n\n it('should include upload URL for each static asset', async () => {\n const result = await createDeployment('mock-token', 'tenant_456', {\n staticAssets: ['_next/static/abc.js', '_next/static/xyz.css'],\n })\n\n expect(result.staticAssetUploadUrls).toHaveProperty('_next/static/abc.js')\n expect(result.staticAssetUploadUrls).toHaveProperty('_next/static/xyz.css')\n expect(result.staticAssetUploadUrls['_next/static/abc.js']).toContain('https://')\n })\n\n it('should return S3 upload URL format', async () => {\n const result = await createDeployment('mock-token', 'tenant_789', {\n staticAssets: ['_next/static/test.js'],\n })\n\n expect(result.codeUploadUrl).toMatch(/^https:\\/\\/.*\\.s3\\..*\\.amazonaws\\.com\\//)\n expect(result.staticAssetUploadUrls['_next/static/test.js']).toMatch(\n /^https:\\/\\/.*\\.s3\\..*\\.amazonaws\\.com\\//,\n )\n })\n\n it('should handle empty static assets array', async () => {\n const result = await createDeployment('mock-token', 'tenant_empty', {\n staticAssets: [],\n })\n\n expect(result.deploymentId).toBeDefined()\n expect(result.codeUploadUrl).toBeDefined()\n expect(Object.keys(result.staticAssetUploadUrls)).toHaveLength(0)\n })\n\n it('should generate unique deployment IDs', async () => {\n const result1 = await createDeployment('mock-token', 'tenant_abc', {\n staticAssets: [],\n })\n const result2 = await createDeployment('mock-token', 'tenant_abc', {\n staticAssets: [],\n })\n\n expect(result1.deploymentId).not.toBe(result2.deploymentId)\n })\n })\n\n describe('performDeployment', () => {\n it('should deploy using deployment ID', async () => {\n const result = await performDeployment('mock-token', 'tenant_123', {\n deploymentId: 'deploy_abc123',\n })\n\n expect(result).toHaveProperty('deploymentId')\n expect(result).toHaveProperty('message')\n expect(result.deploymentId).toBe('deploy_abc123')\n })\n\n it('should return success message', async () => {\n const result = await performDeployment('mock-token', 'tenant_456', {\n deploymentId: 'deploy_xyz789',\n })\n\n expect(result.message).toBeDefined()\n expect(result.message).toBe('Deployment initiated successfully')\n expect(result.message.length).toBeGreaterThan(0)\n })\n\n it('should handle different tenant IDs', async () => {\n const result1 = await performDeployment('mock-token', 'tenant_aaa', {\n deploymentId: 'deploy_1',\n })\n const result2 = await performDeployment('mock-token', 'tenant_bbb', {\n deploymentId: 'deploy_2',\n })\n\n expect(result1.deploymentId).toBe('deploy_1')\n expect(result2.deploymentId).toBe('deploy_2')\n })\n })\n\n describe('error handling', () => {\n beforeEach(() => {\n // Force real API mode for error testing\n process.env.FIGMA_MOCK_CONTROL_PLANE = 'false'\n })\n\n afterEach(() => {\n // Restore mock mode\n delete process.env.FIGMA_MOCK_CONTROL_PLANE\n })\n\n it('createDeployment should throw ControlPlaneError on API failure', async () => {\n // Mock fetch to return error\n global.fetch = vi.fn().mockResolvedValue({\n ok: false,\n status: 500,\n statusText: 'Internal Server Error',\n })\n\n await expect(\n createDeployment('bad-token', 'tenant_fail', { staticAssets: [] }),\n ).rejects.toThrow('Failed to create deployment: 500 Internal Server Error')\n })\n\n it('performDeployment should throw ControlPlaneError on API failure', async () => {\n // Mock fetch to return error\n global.fetch = vi.fn().mockResolvedValue({\n ok: false,\n status: 404,\n statusText: 'Not Found',\n })\n\n await expect(\n performDeployment('bad-token', 'tenant_missing', {\n deploymentId: 'deploy_notfound',\n }),\n ).rejects.toThrow('Failed to perform deployment: 404 Not Found')\n })\n\n it('createDeployment should map snake_case API response to camelCase', async () => {\n // Mock fetch to return successful API response with snake_case\n global.fetch = vi.fn().mockResolvedValue({\n json: () => ({\n error: false,\n meta: {\n code_upload_url: 'https://s3.amazonaws.com/code/upload',\n deployment_id: 'deploy_test123',\n static_asset_upload_urls: {\n '_next/static/test.js': 'https://s3.amazonaws.com/static/test.js',\n },\n },\n status: 200,\n }),\n ok: true,\n })\n\n const result = await createDeployment('token', 'tenant_123', {\n staticAssets: ['_next/static/test.js'],\n })\n\n // Verify response is mapped to camelCase\n expect(result.deploymentId).toBe('deploy_test123')\n expect(result.codeUploadUrl).toBe('https://s3.amazonaws.com/code/upload')\n expect(result.staticAssetUploadUrls).toEqual({\n '_next/static/test.js': 'https://s3.amazonaws.com/static/test.js',\n })\n })\n\n it('performDeployment should map snake_case API response to camelCase', async () => {\n // Mock fetch to return successful API response with snake_case\n global.fetch = vi.fn().mockResolvedValue({\n json: () => ({\n error: false,\n meta: {\n deployment_id: 'deploy_abc456',\n message: 'Deployment successful',\n status: 'deployed',\n },\n status: 200,\n }),\n ok: true,\n })\n\n const result = await performDeployment('token', 'tenant_456', {\n deploymentId: 'deploy_abc456',\n })\n\n // Verify response is mapped to camelCase\n expect(result.deploymentId).toBe('deploy_abc456')\n expect(result.message).toBe('Deployment successful')\n })\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","createDeployment","createTenant","getTenantDetails","listTenants","performDeployment","clearAllMocks","restoreAllMocks","tenants","Array","isArray","toBe","length","toBeGreaterThan","forEach","tenant","toHaveProperty","toContain","status","parentType","tenants1","tenants2","name","domain","tenant1","tenant2","id","not","toMatch","parentId","createdAt","toBeDefined","Date","getTime","result","staticAssets","deploymentId","staticAssetUploadUrls","codeUploadUrl","Object","keys","toHaveLength","result1","result2","message","process","env","FIGMA_MOCK_CONTROL_PLANE","global","fetch","fn","mockResolvedValue","ok","statusText","rejects","toThrow","json","error","meta","code_upload_url","deployment_id","static_asset_upload_urls","toEqual"],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma API Client
|
|
3
|
+
*
|
|
4
|
+
* Client for Figma REST API (Pixie) endpoints
|
|
5
|
+
* Handles project token generation and other CMS-related API calls
|
|
6
|
+
*/
|
|
7
|
+
import type { ProjectToken } from '../auth/types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when Figma API calls fail
|
|
10
|
+
*/
|
|
11
|
+
export declare class FigmaApiError extends Error {
|
|
12
|
+
statusCode?: number | undefined;
|
|
13
|
+
cause?: Error | undefined;
|
|
14
|
+
constructor(message: string, statusCode?: number | undefined, cause?: Error | undefined);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get a project token (JWT) for a specific tenant/CMS
|
|
18
|
+
*
|
|
19
|
+
* This token is used by local Payload instances to authenticate
|
|
20
|
+
* requests to the Content API. The token contains the CMS ID and
|
|
21
|
+
* user identifying information in its claims.
|
|
22
|
+
*
|
|
23
|
+
* Endpoint: GET https://api.figma.com/v1/cms/:id/token/
|
|
24
|
+
*
|
|
25
|
+
* @param accessToken - OAuth access token for authentication
|
|
26
|
+
* @param tenantId - The tenant/CMS ID
|
|
27
|
+
* @returns ProjectToken with JWT and expiry information
|
|
28
|
+
* @throws {FigmaApiError} If the API call fails
|
|
29
|
+
*/
|
|
30
|
+
export declare function getProjectToken(accessToken: string, tenantId: string): Promise<ProjectToken>;
|
|
31
|
+
//# sourceMappingURL=figma-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figma-api.d.ts","sourceRoot":"","sources":["../../src/api/figma-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAwB,MAAM,kBAAkB,CAAA;AAI1E;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAG7B,UAAU,CAAC,EAAE,MAAM;IACnB,KAAK,CAAC,EAAE,KAAK;gBAFpB,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAmBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAuFvB"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma API Client
|
|
3
|
+
*
|
|
4
|
+
* Client for Figma REST API (Pixie) endpoints
|
|
5
|
+
* Handles project token generation and other CMS-related API calls
|
|
6
|
+
*/ import * as log from '../utils/log.js';
|
|
7
|
+
/**
|
|
8
|
+
* Error thrown when Figma API calls fail
|
|
9
|
+
*/ export class FigmaApiError extends Error {
|
|
10
|
+
statusCode;
|
|
11
|
+
cause;
|
|
12
|
+
constructor(message, statusCode, cause){
|
|
13
|
+
super(message), this.statusCode = statusCode, this.cause = cause;
|
|
14
|
+
this.name = 'FigmaApiError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Base URLs for Figma API
|
|
19
|
+
*/ const API_BASE_URLS = {
|
|
20
|
+
prod: 'https://api.figma.com',
|
|
21
|
+
staging: 'https://api.staging.figma.com'
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Get the Figma API base URL based on environment
|
|
25
|
+
* TODO: Use this once real API endpoints are available
|
|
26
|
+
*/ function _getApiBaseUrl() {
|
|
27
|
+
// For now, default to staging to match OAuth config
|
|
28
|
+
return process.env.FIGMA_API_BASE_URL || API_BASE_URLS.staging;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get a project token (JWT) for a specific tenant/CMS
|
|
32
|
+
*
|
|
33
|
+
* This token is used by local Payload instances to authenticate
|
|
34
|
+
* requests to the Content API. The token contains the CMS ID and
|
|
35
|
+
* user identifying information in its claims.
|
|
36
|
+
*
|
|
37
|
+
* Endpoint: GET https://api.figma.com/v1/cms/:id/token/
|
|
38
|
+
*
|
|
39
|
+
* @param accessToken - OAuth access token for authentication
|
|
40
|
+
* @param tenantId - The tenant/CMS ID
|
|
41
|
+
* @returns ProjectToken with JWT and expiry information
|
|
42
|
+
* @throws {FigmaApiError} If the API call fails
|
|
43
|
+
*/ export async function getProjectToken(accessToken, tenantId) {
|
|
44
|
+
// Check if project token API should be mocked
|
|
45
|
+
// Default to true until real API is available
|
|
46
|
+
const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
|
|
47
|
+
try {
|
|
48
|
+
if (shouldMock) {
|
|
49
|
+
// MOCK IMPLEMENTATION: Generate a mock JWT token for testing
|
|
50
|
+
log.debug('Using mock project token API for getProjectToken (FIGMA_MOCK_PROJECT_TOKEN != "false")');
|
|
51
|
+
const mockToken = createMockJWT(tenantId, 25) // 25 minutes expiry
|
|
52
|
+
;
|
|
53
|
+
// Mock response data
|
|
54
|
+
const data = {
|
|
55
|
+
meta: {
|
|
56
|
+
token: mockToken
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
if (!data.meta?.token) {
|
|
60
|
+
throw new FigmaApiError('Invalid response: missing token in meta');
|
|
61
|
+
}
|
|
62
|
+
// Parse the JWT to extract expiry (it's in the payload)
|
|
63
|
+
const expiresAt = parseJwtExpiry(data.meta.token);
|
|
64
|
+
return {
|
|
65
|
+
expiresAt,
|
|
66
|
+
tenantId,
|
|
67
|
+
token: data.meta.token
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// REAL API IMPLEMENTATION
|
|
71
|
+
const baseUrl = _getApiBaseUrl();
|
|
72
|
+
const url = `${baseUrl}/v1/cms/${tenantId}/token/`;
|
|
73
|
+
const response = await fetch(url, {
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${accessToken}`
|
|
76
|
+
},
|
|
77
|
+
method: 'POST'
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
// Handle error responses
|
|
81
|
+
let errorMessage = `Failed to get project token: ${response.status} ${response.statusText}`;
|
|
82
|
+
try {
|
|
83
|
+
const errorBody = await response.json();
|
|
84
|
+
if (errorBody.error || errorBody.message) {
|
|
85
|
+
errorMessage = errorBody.error || errorBody.message;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Ignore JSON parse errors for error body
|
|
89
|
+
}
|
|
90
|
+
throw new FigmaApiError(errorMessage, response.status);
|
|
91
|
+
}
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
if (!data.meta?.token) {
|
|
94
|
+
throw new FigmaApiError('Invalid response: missing token in meta');
|
|
95
|
+
}
|
|
96
|
+
// Parse the JWT to extract expiry (it's in the payload)
|
|
97
|
+
const expiresAt = parseJwtExpiry(data.meta.token);
|
|
98
|
+
return {
|
|
99
|
+
expiresAt,
|
|
100
|
+
tenantId,
|
|
101
|
+
token: data.meta.token
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof FigmaApiError) {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
// Handle network errors and other unexpected errors
|
|
108
|
+
throw new FigmaApiError(`Failed to get project token: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error instanceof Error ? error : undefined);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a mock JWT token for testing
|
|
113
|
+
* TODO: Remove this when real API is available
|
|
114
|
+
*/ function createMockJWT(tenantId, expiresInMinutes) {
|
|
115
|
+
const header = {
|
|
116
|
+
alg: 'RS256',
|
|
117
|
+
typ: 'JWT'
|
|
118
|
+
};
|
|
119
|
+
const now = Math.floor(Date.now() / 1000) // Current time in seconds
|
|
120
|
+
;
|
|
121
|
+
const exp = now + expiresInMinutes * 60 // Expiry in seconds
|
|
122
|
+
;
|
|
123
|
+
const payload = {
|
|
124
|
+
aud: tenantId,
|
|
125
|
+
email: 'developer@example.com',
|
|
126
|
+
exp,
|
|
127
|
+
iat: now,
|
|
128
|
+
sub: 'user123'
|
|
129
|
+
};
|
|
130
|
+
// Base64url encode
|
|
131
|
+
const encodeBase64Url = (obj)=>{
|
|
132
|
+
return Buffer.from(JSON.stringify(obj)).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
133
|
+
};
|
|
134
|
+
const headerEncoded = encodeBase64Url(header);
|
|
135
|
+
const payloadEncoded = encodeBase64Url(payload);
|
|
136
|
+
const signature = 'mock_signature_' + Math.random().toString(36).substring(2, 15);
|
|
137
|
+
return `${headerEncoded}.${payloadEncoded}.${signature}`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Parse JWT expiry from token
|
|
141
|
+
* JWTs contain an 'exp' claim in the payload (second part)
|
|
142
|
+
*
|
|
143
|
+
* @param token - JWT token string
|
|
144
|
+
* @returns Unix timestamp in milliseconds when token expires
|
|
145
|
+
* @throws {FigmaApiError} If JWT cannot be parsed
|
|
146
|
+
*/ function parseJwtExpiry(token) {
|
|
147
|
+
try {
|
|
148
|
+
// JWT format: header.payload.signature
|
|
149
|
+
const parts = token.split('.');
|
|
150
|
+
if (parts.length !== 3) {
|
|
151
|
+
throw new Error('Invalid JWT format');
|
|
152
|
+
}
|
|
153
|
+
// Decode the payload (second part)
|
|
154
|
+
const payload = parts[1];
|
|
155
|
+
// Use atob for base64url decoding (handle padding and URL-safe characters)
|
|
156
|
+
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
157
|
+
const paddedBase64 = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
|
|
158
|
+
const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8'));
|
|
159
|
+
if (!decodedPayload.exp || typeof decodedPayload.exp !== 'number') {
|
|
160
|
+
throw new Error('JWT payload missing exp claim');
|
|
161
|
+
}
|
|
162
|
+
// JWT exp claim is in seconds, convert to milliseconds
|
|
163
|
+
return decodedPayload.exp * 1000;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new FigmaApiError(`Failed to parse JWT expiry: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error instanceof Error ? error : undefined);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=figma-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/figma-api.ts"],"sourcesContent":["/**\n * Figma API Client\n *\n * Client for Figma REST API (Pixie) endpoints\n * Handles project token generation and other CMS-related API calls\n */\n\nimport type { ProjectToken, ProjectTokenResponse } from '../auth/types.js'\n\nimport * as log from '../utils/log.js'\n\n/**\n * Error thrown when Figma API calls fail\n */\nexport class FigmaApiError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'FigmaApiError'\n }\n}\n\n/**\n * Base URLs for Figma API\n */\nconst API_BASE_URLS = {\n prod: 'https://api.figma.com',\n staging: 'https://api.staging.figma.com',\n}\n\n/**\n * Get the Figma API base URL based on environment\n * TODO: Use this once real API endpoints are available\n */\nfunction _getApiBaseUrl(): string {\n // For now, default to staging to match OAuth config\n return process.env.FIGMA_API_BASE_URL || API_BASE_URLS.staging\n}\n\n/**\n * Get a project token (JWT) for a specific tenant/CMS\n *\n * This token is used by local Payload instances to authenticate\n * requests to the Content API. The token contains the CMS ID and\n * user identifying information in its claims.\n *\n * Endpoint: GET https://api.figma.com/v1/cms/:id/token/\n *\n * @param accessToken - OAuth access token for authentication\n * @param tenantId - The tenant/CMS ID\n * @returns ProjectToken with JWT and expiry information\n * @throws {FigmaApiError} If the API call fails\n */\nexport async function getProjectToken(\n accessToken: string,\n tenantId: string,\n): Promise<ProjectToken> {\n // Check if project token API should be mocked\n // Default to true until real API is available\n const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false'\n\n try {\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Generate a mock JWT token for testing\n log.debug(\n 'Using mock project token API for getProjectToken (FIGMA_MOCK_PROJECT_TOKEN != \"false\")',\n )\n const mockToken = createMockJWT(tenantId, 25) // 25 minutes expiry\n\n // Mock response data\n const data: ProjectTokenResponse = {\n meta: {\n token: mockToken,\n },\n }\n\n if (!data.meta?.token) {\n throw new FigmaApiError('Invalid response: missing token in meta')\n }\n\n // Parse the JWT to extract expiry (it's in the payload)\n const expiresAt = parseJwtExpiry(data.meta.token)\n\n return {\n expiresAt,\n tenantId,\n token: data.meta.token,\n }\n }\n\n // REAL API IMPLEMENTATION\n const baseUrl = _getApiBaseUrl()\n const url = `${baseUrl}/v1/cms/${tenantId}/token/`\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n // Handle error responses\n let errorMessage = `Failed to get project token: ${response.status} ${response.statusText}`\n\n try {\n const errorBody = await response.json()\n if (errorBody.error || errorBody.message) {\n errorMessage = errorBody.error || errorBody.message\n }\n } catch {\n // Ignore JSON parse errors for error body\n }\n\n throw new FigmaApiError(errorMessage, response.status)\n }\n\n const data: ProjectTokenResponse = await response.json()\n\n if (!data.meta?.token) {\n throw new FigmaApiError('Invalid response: missing token in meta')\n }\n\n // Parse the JWT to extract expiry (it's in the payload)\n const expiresAt = parseJwtExpiry(data.meta.token)\n\n return {\n expiresAt,\n tenantId,\n token: data.meta.token,\n }\n } catch (error) {\n if (error instanceof FigmaApiError) {\n throw error\n }\n\n // Handle network errors and other unexpected errors\n throw new FigmaApiError(\n `Failed to get project token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n undefined,\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Create a mock JWT token for testing\n * TODO: Remove this when real API is available\n */\nfunction createMockJWT(tenantId: string, expiresInMinutes: number): string {\n const header = {\n alg: 'RS256',\n typ: 'JWT',\n }\n\n const now = Math.floor(Date.now() / 1000) // Current time in seconds\n const exp = now + expiresInMinutes * 60 // Expiry in seconds\n\n const payload = {\n aud: tenantId, // Audience claim contains the CMS ID\n email: 'developer@example.com',\n exp,\n iat: now,\n sub: 'user123',\n }\n\n // Base64url encode\n const encodeBase64Url = (obj: object): string => {\n return Buffer.from(JSON.stringify(obj))\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n }\n\n const headerEncoded = encodeBase64Url(header)\n const payloadEncoded = encodeBase64Url(payload)\n const signature = 'mock_signature_' + Math.random().toString(36).substring(2, 15)\n\n return `${headerEncoded}.${payloadEncoded}.${signature}`\n}\n\n/**\n * Parse JWT expiry from token\n * JWTs contain an 'exp' claim in the payload (second part)\n *\n * @param token - JWT token string\n * @returns Unix timestamp in milliseconds when token expires\n * @throws {FigmaApiError} If JWT cannot be parsed\n */\nfunction parseJwtExpiry(token: string): number {\n try {\n // JWT format: header.payload.signature\n const parts = token.split('.')\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format')\n }\n\n // Decode the payload (second part)\n const payload = parts[1]\n // Use atob for base64url decoding (handle padding and URL-safe characters)\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')\n\n const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8')) as {\n exp?: number\n }\n\n if (!decodedPayload.exp || typeof decodedPayload.exp !== 'number') {\n throw new Error('JWT payload missing exp claim')\n }\n\n // JWT exp claim is in seconds, convert to milliseconds\n return decodedPayload.exp * 1000\n } catch (error) {\n throw new FigmaApiError(\n `Failed to parse JWT expiry: ${error instanceof Error ? error.message : 'Unknown error'}`,\n undefined,\n error instanceof Error ? error : undefined,\n )\n }\n}\n"],"names":["log","FigmaApiError","Error","message","statusCode","cause","name","API_BASE_URLS","prod","staging","_getApiBaseUrl","process","env","FIGMA_API_BASE_URL","getProjectToken","accessToken","tenantId","shouldMock","FIGMA_MOCK_PROJECT_TOKEN","debug","mockToken","createMockJWT","data","meta","token","expiresAt","parseJwtExpiry","baseUrl","url","response","fetch","headers","Authorization","method","ok","errorMessage","status","statusText","errorBody","json","error","undefined","expiresInMinutes","header","alg","typ","now","Math","floor","Date","exp","payload","aud","email","iat","sub","encodeBase64Url","obj","Buffer","from","JSON","stringify","toString","replace","headerEncoded","payloadEncoded","signature","random","substring","parts","split","length","base64","paddedBase64","padEnd","decodedPayload","parse"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests JWT parsing helpers and validates mock API contract.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: The Figma API is currently mocked for development.
|
|
5
|
+
*
|
|
6
|
+
* These tests verify:
|
|
7
|
+
* 1. JWT parsing helper functions (our code)
|
|
8
|
+
* 2. JWT structure matches expected Figma API contract
|
|
9
|
+
* 3. Mock responses are consistent for development
|
|
10
|
+
*
|
|
11
|
+
* TODO: Replace with integration tests when real API is available
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=figma-api.spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figma-api.spec.d.ts","sourceRoot":"","sources":["../../src/api/figma-api.spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|