@airdraft/cloud 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/billing.d.ts +72 -0
  3. package/dist/billing.d.ts.map +1 -0
  4. package/dist/billing.js +204 -0
  5. package/dist/billing.js.map +1 -0
  6. package/dist/callback.d.ts +39 -0
  7. package/dist/callback.d.ts.map +1 -0
  8. package/dist/callback.js +178 -0
  9. package/dist/callback.js.map +1 -0
  10. package/dist/cli.d.ts +18 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +59 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/db.d.ts +13 -0
  15. package/dist/db.d.ts.map +1 -0
  16. package/dist/db.js +23 -0
  17. package/dist/db.js.map +1 -0
  18. package/dist/index.d.ts +20 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +19 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/invite.d.ts +28 -0
  23. package/dist/invite.d.ts.map +1 -0
  24. package/dist/invite.js +155 -0
  25. package/dist/invite.js.map +1 -0
  26. package/dist/jwt.d.ts +9 -0
  27. package/dist/jwt.d.ts.map +1 -0
  28. package/dist/jwt.js +32 -0
  29. package/dist/jwt.js.map +1 -0
  30. package/dist/project.d.ts +30 -0
  31. package/dist/project.d.ts.map +1 -0
  32. package/dist/project.js +145 -0
  33. package/dist/project.js.map +1 -0
  34. package/dist/relay.d.ts +35 -0
  35. package/dist/relay.d.ts.map +1 -0
  36. package/dist/relay.js +101 -0
  37. package/dist/relay.js.map +1 -0
  38. package/dist/runtime.d.ts +48 -0
  39. package/dist/runtime.d.ts.map +1 -0
  40. package/dist/runtime.js +186 -0
  41. package/dist/runtime.js.map +1 -0
  42. package/dist/state.d.ts +40 -0
  43. package/dist/state.d.ts.map +1 -0
  44. package/dist/state.js +58 -0
  45. package/dist/state.js.map +1 -0
  46. package/dist/team.d.ts +31 -0
  47. package/dist/team.d.ts.map +1 -0
  48. package/dist/team.js +150 -0
  49. package/dist/team.js.map +1 -0
  50. package/dist/types.d.ts +161 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/webhook.d.ts +29 -0
  55. package/dist/webhook.d.ts.map +1 -0
  56. package/dist/webhook.js +125 -0
  57. package/dist/webhook.js.map +1 -0
  58. package/package.json +45 -0
package/dist/relay.js ADDED
@@ -0,0 +1,101 @@
1
+ import { hashApiKey } from '@airdraft/auth';
2
+ import { signGitHubAppJwt } from './jwt.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Helpers
5
+ // ---------------------------------------------------------------------------
6
+ const GITHUB_API = 'https://api.github.com';
7
+ const CACHE_TTL_S = 55 * 60; // 55 minutes — 5 min buffer before GitHub's 1-hr expiry
8
+ const RATE_LIMIT_KEY = (installationId) => `ratelimit:github:token:${installationId}`;
9
+ const TOKEN_CACHE_KEY = (installationId) => `github:token:${installationId}`;
10
+ function json(body, status = 200) {
11
+ return new Response(JSON.stringify(body), {
12
+ status,
13
+ headers: { 'Content-Type': 'application/json' },
14
+ });
15
+ }
16
+ // ---------------------------------------------------------------------------
17
+ // handleTokenRelay
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Handler for `POST /v1/github/token`.
21
+ *
22
+ * Exchanges a project API key (`ntk_*`) for a GitHub installation access token.
23
+ * Used by mode 2 self-hosted runtimes that rely on Airdraft's shared GitHub App.
24
+ *
25
+ * Flow:
26
+ * 1. Extract and validate `ntk_*` key from `Authorization: Bearer` header
27
+ * 2. Hash → look up in `projects` collection
28
+ * 3. Apply per-installation rate limit (1 req/s, Redis counter)
29
+ * 4. Return cached token from Redis if still fresh
30
+ * 5. Mint fresh token via GitHub App JWT → cache → return
31
+ *
32
+ * ```ts
33
+ * // In a Next.js route handler:
34
+ * export async function POST(req: Request) {
35
+ * return handleTokenRelay(req, db, {
36
+ * githubAppId: process.env.GITHUB_APP_ID!,
37
+ * githubAppPrivateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
38
+ * cache: redisCache,
39
+ * })
40
+ * }
41
+ * ```
42
+ */
43
+ export async function handleTokenRelay(req, db, opts) {
44
+ // 1. Extract API key from Authorization header
45
+ const authHeader = req.headers.get('Authorization') ?? '';
46
+ const apiKey = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : null;
47
+ if (!apiKey || !apiKey.startsWith('ntk_')) {
48
+ return json({ error: 'INVALID_KEY' }, 401);
49
+ }
50
+ // 2. Hash → project lookup
51
+ const hash = hashApiKey(apiKey);
52
+ const project = await db.projects.findOne({ apiKeyHash: hash });
53
+ if (!project)
54
+ return json({ error: 'INVALID_KEY' }, 401);
55
+ if (!project.github)
56
+ return json({ error: 'GITHUB_NOT_CONNECTED' }, 404);
57
+ const { installationId } = project.github;
58
+ // 3. Rate limit: 1 req/installation/sec via Redis counter
59
+ if (opts.cache) {
60
+ const rlKey = RATE_LIMIT_KEY(installationId);
61
+ const count = await opts.cache.get(rlKey);
62
+ if (count !== null && parseInt(count, 10) >= 1) {
63
+ return json({ error: 'RATE_LIMITED' }, 429);
64
+ }
65
+ await opts.cache.set(rlKey, '1', 1); // TTL: 1 second
66
+ }
67
+ // 4. L2 cache check
68
+ if (opts.cache) {
69
+ const cached = await opts.cache.get(TOKEN_CACHE_KEY(installationId));
70
+ if (cached) {
71
+ return json(JSON.parse(cached));
72
+ }
73
+ }
74
+ // 5. Mint fresh token via GitHub App JWT
75
+ let githubRes;
76
+ try {
77
+ const jwt = await signGitHubAppJwt(opts.githubAppId, opts.githubAppPrivateKey);
78
+ githubRes = await fetch(`${GITHUB_API}/app/installations/${installationId}/access_tokens`, {
79
+ method: 'POST',
80
+ headers: {
81
+ Authorization: `Bearer ${jwt}`,
82
+ Accept: 'application/vnd.github+json',
83
+ 'X-GitHub-Api-Version': '2022-11-28',
84
+ },
85
+ });
86
+ }
87
+ catch {
88
+ return json({ error: 'GITHUB_UNAVAILABLE' }, 503);
89
+ }
90
+ if (!githubRes.ok) {
91
+ return json({ error: 'GITHUB_UNAVAILABLE' }, 503);
92
+ }
93
+ const body = await githubRes.json();
94
+ const result = { token: body.token, expiresAt: body.expires_at };
95
+ // Cache for 55 minutes
96
+ if (opts.cache) {
97
+ await opts.cache.set(TOKEN_CACHE_KEY(installationId), JSON.stringify(result), CACHE_TTL_S);
98
+ }
99
+ return json(result);
100
+ }
101
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { CloudAuthAdapter } from '@airdraft/cloud-auth';
2
+ import type { CloudDb, TokenCache } from './types.js';
3
+ /**
4
+ * Evicts the cached CmsEngine for a project. Called by `handleWebhook` on
5
+ * push events so the next request picks up the updated schema immediately.
6
+ */
7
+ export declare function bustEngineCache(projectId: string): void;
8
+ export interface CloudCmsOptions {
9
+ db: CloudDb;
10
+ /** TokenCache used for GitHub token caching AND schema caching. */
11
+ schemaCache: TokenCache;
12
+ githubAppId: string;
13
+ githubAppPrivateKey: string;
14
+ /**
15
+ * Cloud auth adapter. When provided, Bearer JWTs from cloud dashboard
16
+ * sessions are accepted alongside ntk_* API keys.
17
+ */
18
+ authAdapter?: CloudAuthAdapter;
19
+ }
20
+ interface RouteContext {
21
+ params: Promise<{
22
+ projectSlug: string;
23
+ route?: string[];
24
+ }>;
25
+ }
26
+ type HandlerFn = (req: Request, ctx: RouteContext) => Promise<Response>;
27
+ export interface CloudCmsHandler {
28
+ GET: HandlerFn;
29
+ POST: HandlerFn;
30
+ PUT: HandlerFn;
31
+ PATCH: HandlerFn;
32
+ DELETE: HandlerFn;
33
+ }
34
+ /**
35
+ * Creates Next.js App Router route handlers for the cloud CMS runtime.
36
+ *
37
+ * Mount at `app/(cms)/[projectSlug]/api/cms/[...route]/route.ts`:
38
+ *
39
+ * ```ts
40
+ * const { GET, POST, PUT, DELETE } = createCloudCmsHandler({ db, schemaCache, githubAppId, githubAppPrivateKey })
41
+ * export { GET, POST, PUT, DELETE }
42
+ * ```
43
+ *
44
+ * URL pattern: `cms.airdraft.space/{projectSlug}/api/cms/{collection}/{slug?}`
45
+ */
46
+ export declare function createCloudCmsHandler(opts: CloudCmsOptions): CloudCmsHandler;
47
+ export {};
48
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAc,MAAM,YAAY,CAAA;AAejE;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEvD;AAMD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAA;IACX,mEAAmE;IACnE,WAAW,EAAE,UAAU,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;CAC/B;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;CAC3D;AAED,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEvE,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,SAAS,CAAA;IACd,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,SAAS,CAAA;IACd,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,SAAS,CAAA;CAClB;AAwID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAwD5E"}
@@ -0,0 +1,186 @@
1
+ import { GitHubAdapter } from '@airdraft/core';
2
+ import { createCmsHandler } from '@airdraft/next';
3
+ import { withAuth } from '@airdraft/plugin-auth';
4
+ import { withAuditLog } from '@airdraft/plugin-audit-log';
5
+ import { hashApiKey } from '@airdraft/auth';
6
+ const _engineCache = new Map();
7
+ const ENGINE_TTL_MS = 5 * 60 * 1000; // 5 minutes
8
+ /**
9
+ * Evicts the cached CmsEngine for a project. Called by `handleWebhook` on
10
+ * push events so the next request picks up the updated schema immediately.
11
+ */
12
+ export function bustEngineCache(projectId) {
13
+ _engineCache.delete(projectId);
14
+ }
15
+ const SCHEMA_CACHE_PREFIX = 'schema:';
16
+ function json(body, status = 200) {
17
+ return new Response(JSON.stringify(body), {
18
+ status,
19
+ headers: { 'Content-Type': 'application/json' },
20
+ });
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // buildProjectHandler
24
+ // ---------------------------------------------------------------------------
25
+ async function buildProjectHandler(project, opts) {
26
+ if (!project || !project.github)
27
+ throw new Error('Project or GitHub connection missing');
28
+ const { appId, repo, branch, installationId } = {
29
+ appId: opts.githubAppId,
30
+ repo: project.github.repo,
31
+ branch: project.github.branch,
32
+ installationId: String(project.github.installationId),
33
+ };
34
+ // Build GitHub adapter (direct JWT signing — cloud app owns the GitHub App)
35
+ const adapter = new GitHubAdapter({
36
+ appId,
37
+ privateKey: opts.githubAppPrivateKey,
38
+ installationId,
39
+ repo,
40
+ branch,
41
+ });
42
+ // Load schema JSON — check Redis cache first
43
+ const schemaCacheKey = `${SCHEMA_CACHE_PREFIX}${project._id.toString()}`;
44
+ let schemaJson = null;
45
+ if (opts.schemaCache) {
46
+ schemaJson = await opts.schemaCache.get(schemaCacheKey);
47
+ }
48
+ if (!schemaJson) {
49
+ const file = await adapter.read('airdraft.schema.json');
50
+ if (!file)
51
+ throw new Error('airdraft.schema.json not found in repository');
52
+ schemaJson = file.content;
53
+ await opts.schemaCache?.set(schemaCacheKey, schemaJson, 5 * 60); // 5-min TTL
54
+ }
55
+ const schema = JSON.parse(schemaJson);
56
+ // Build unified auth provider
57
+ const projectApiKeyHash = project.apiKeyHash;
58
+ const projectTeamId = project.teamId;
59
+ const db = opts.db;
60
+ const authAdapter = opts.authAdapter;
61
+ const authProvider = {
62
+ async verify(req) {
63
+ const authHeader = req.headers.get('Authorization') ?? '';
64
+ // 1. ntk_* API key — hash compare against stored hash
65
+ if (authHeader.startsWith('Bearer ntk_')) {
66
+ const key = authHeader.slice(7).trim();
67
+ if (projectApiKeyHash && hashApiKey(key) === projectApiKeyHash) {
68
+ return { id: 'api-key', email: 'api@key', name: 'API Key', role: 'admin' };
69
+ }
70
+ return null;
71
+ }
72
+ // 2. Cloud session — Bearer JWT or httpOnly cookie via CloudAuthAdapter.
73
+ // Called regardless of Authorization header presence so that dashboard
74
+ // pages using same-origin cookie auth (no Bearer header) also pass.
75
+ if (authAdapter) {
76
+ const identity = await authAdapter.verifySession(req);
77
+ if (!identity)
78
+ return null;
79
+ const membership = await db.memberships.findOne({
80
+ teamId: projectTeamId,
81
+ userId: identity.userId,
82
+ });
83
+ if (!membership)
84
+ return null;
85
+ // Map cloud role to CMS role
86
+ const roleMap = {
87
+ owner: 'admin',
88
+ admin: 'admin',
89
+ editor: 'publisher',
90
+ viewer: 'editor',
91
+ };
92
+ const role = roleMap[membership.role] ?? 'editor';
93
+ return { id: identity.userId, email: identity.email, name: identity.name, role };
94
+ }
95
+ return null;
96
+ },
97
+ };
98
+ // Build CmsConfig — include MongoDB audit persister when auditEvents collection available
99
+ const plugins = [withAuth({ provider: authProvider })];
100
+ if (opts.db.auditEvents) {
101
+ const projectId = project._id.toString();
102
+ plugins.push(withAuditLog({
103
+ persist: async (event) => {
104
+ try {
105
+ await opts.db.auditEvents.insertOne({
106
+ ...event,
107
+ projectId,
108
+ });
109
+ }
110
+ catch {
111
+ // audit log failures must never crash the CMS handler
112
+ }
113
+ },
114
+ }));
115
+ }
116
+ const config = {
117
+ adapter,
118
+ collections: schema.collections,
119
+ plugins,
120
+ basePath: '/api/cms',
121
+ };
122
+ return createCmsHandler(config);
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // createCloudCmsHandler
126
+ // ---------------------------------------------------------------------------
127
+ /**
128
+ * Creates Next.js App Router route handlers for the cloud CMS runtime.
129
+ *
130
+ * Mount at `app/(cms)/[projectSlug]/api/cms/[...route]/route.ts`:
131
+ *
132
+ * ```ts
133
+ * const { GET, POST, PUT, DELETE } = createCloudCmsHandler({ db, schemaCache, githubAppId, githubAppPrivateKey })
134
+ * export { GET, POST, PUT, DELETE }
135
+ * ```
136
+ *
137
+ * URL pattern: `cms.airdraft.space/{projectSlug}/api/cms/{collection}/{slug?}`
138
+ */
139
+ export function createCloudCmsHandler(opts) {
140
+ async function handle(req, ctx) {
141
+ const params = await ctx.params;
142
+ const projectSlug = params.projectSlug;
143
+ const route = params.route ?? [];
144
+ // Load project by slug
145
+ const project = await opts.db.projects.findOne({ slug: projectSlug });
146
+ if (!project) {
147
+ return json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } }, 404);
148
+ }
149
+ if (!project.github) {
150
+ return json({ error: { code: 'GITHUB_NOT_CONNECTED', message: 'GitHub App not connected to this project' } }, 404);
151
+ }
152
+ // Get or build cached handler
153
+ const projectId = project._id.toString();
154
+ const cached = _engineCache.get(projectId);
155
+ let handler;
156
+ if (cached && cached.expiresAt > Date.now()) {
157
+ handler = cached.handler;
158
+ }
159
+ else {
160
+ try {
161
+ handler = await buildProjectHandler(project, opts);
162
+ }
163
+ catch (err) {
164
+ console.error('[airdraft/cloud] Failed to build engine for project', projectSlug, err);
165
+ return json({ error: { code: 'ENGINE_INIT_FAILED', message: 'Failed to initialize CMS engine' } }, 500);
166
+ }
167
+ _engineCache.set(projectId, { handler, expiresAt: Date.now() + ENGINE_TTL_MS });
168
+ }
169
+ // Delegate to the inner CMS handler, adapting the route context
170
+ const innerCtx = { params: Promise.resolve({ route }) };
171
+ const method = req.method.toUpperCase();
172
+ const routeHandler = handler[method];
173
+ if (!routeHandler) {
174
+ return json({ error: { code: 'METHOD_NOT_ALLOWED', message: `${method} not supported` } }, 405);
175
+ }
176
+ return routeHandler(req, innerCtx);
177
+ }
178
+ return {
179
+ GET: handle,
180
+ POST: handle,
181
+ PUT: handle,
182
+ PATCH: handle,
183
+ DELETE: handle,
184
+ };
185
+ }
186
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAc3C,MAAM,YAAY,GAAG,IAAI,GAAG,EAAyB,CAAA;AACrD,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,YAAY;AAEhD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAChC,CAAC;AAiCD,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAErC,SAAS,IAAI,CAAC,IAAa,EAAE,MAAM,GAAG,GAAG;IACvC,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAA;AACJ,CAAC;AAED,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E,KAAK,UAAU,mBAAmB,CAChC,OAA2B,EAC3B,IAAqB;IAErB,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;IAExF,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG;QAC9C,KAAK,EAAE,IAAI,CAAC,WAAW;QACvB,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;QACzB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM;QAC7B,cAAc,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC;KACtD,CAAA;IAED,4EAA4E;IAC5E,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC;QAChC,KAAK;QACL,UAAU,EAAE,IAAI,CAAC,mBAAmB;QACpC,cAAc;QACd,IAAI;QACJ,MAAM;KACP,CAAC,CAAA;IAEF,6CAA6C;IAC7C,MAAM,cAAc,GAAG,GAAG,mBAAmB,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAA;IACxE,IAAI,UAAU,GAAkB,IAAI,CAAA;IAEpC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,UAAU,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;IACzD,CAAC;IAED,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAA;QACvD,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAA;QAC1E,UAAU,GAAG,IAAI,CAAC,OAAO,CAAA;QACzB,MAAM,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,cAAc,EAAE,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,CAAA,CAAC,YAAY;IAC9E,CAAC;IAED,MAAM,MAAM,GAAc,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IAEhD,8BAA8B;IAC9B,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAA;IAC5C,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAA;IACpC,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAA;IAClB,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAA;IAEpC,MAAM,YAAY,GAAiB;QACjC,KAAK,CAAC,MAAM,CAAC,GAAY;YACvB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAA;YAEzD,sDAAsD;YACtD,IAAI,UAAU,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;gBACzC,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;gBACtC,IAAI,iBAAiB,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,iBAAiB,EAAE,CAAC;oBAC/D,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;gBAC5E,CAAC;gBACD,OAAO,IAAI,CAAA;YACb,CAAC;YAED,yEAAyE;YACzE,0EAA0E;YAC1E,uEAAuE;YACvE,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;gBACrD,IAAI,CAAC,QAAQ;oBAAE,OAAO,IAAI,CAAA;gBAE1B,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC;oBAC9C,MAAM,EAAE,aAAa;oBACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;iBACxB,CAAC,CAAA;gBACF,IAAI,CAAC,UAAU;oBAAE,OAAO,IAAI,CAAA;gBAE5B,6BAA6B;gBAC7B,MAAM,OAAO,GAAqC;oBAChD,KAAK,EAAE,OAAO;oBACd,KAAK,EAAE,OAAO;oBACd,MAAM,EAAE,WAAW;oBACnB,MAAM,EAAE,QAAQ;iBACjB,CAAA;gBACD,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAA;gBACjD,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,CAAA;YAClF,CAAC;YAED,OAAO,IAAI,CAAA;QACb,CAAC;KACF,CAAA;IAED,0FAA0F;IAC1F,MAAM,OAAO,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC,CAAA;IAEtD,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;QACxC,OAAO,CAAC,IAAI,CACV,YAAY,CAAC;YACX,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBACvB,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC;wBAClC,GAAG,KAAK;wBACR,SAAS;qBACD,CAAC,CAAA;gBACb,CAAC;gBAAC,MAAM,CAAC;oBACP,sDAAsD;gBACxD,CAAC;YACH,CAAC;SACF,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM,MAAM,GAAc;QACxB,OAAO;QACP,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO;QACP,QAAQ,EAAE,UAAU;KACrB,CAAA;IAED,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAA;AACjC,CAAC;AAED,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAqB;IACzD,KAAK,UAAU,MAAM,CAAC,GAAY,EAAE,GAAiB;QACnD,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAA;QAC/B,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAA;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAA;QAEhC,uBAAuB;QACvB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;QACrE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,mBAAmB,EAAE,EAAE,EAAE,GAAG,CAAC,CAAA;QAC1F,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,OAAO,IAAI,CACT,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,0CAA0C,EAAE,EAAE,EAChG,GAAG,CACJ,CAAA;QACH,CAAC;QAED,8BAA8B;QAC9B,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;QACxC,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC1C,IAAI,OAA4C,CAAA;QAEhD,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC5C,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC;gBACH,OAAO,GAAG,MAAM,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YACpD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,qDAAqD,EAAE,WAAW,EAAE,GAAG,CAAC,CAAA;gBACtF,OAAO,IAAI,CACT,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,iCAAiC,EAAE,EAAE,EACrF,GAAG,CACJ,CAAA;YACH,CAAC;YACD,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,EAAE,CAAC,CAAA;QACjF,CAAC;QAED,gEAAgE;QAChE,MAAM,QAAQ,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;QACvD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,WAAW,EAAiD,CAAA;QACtF,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,GAAG,MAAM,gBAAgB,EAAE,EAAE,EAAE,GAAG,CAAC,CAAA;QACjG,CAAC;QAED,OAAO,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACpC,CAAC;IAED,OAAO;QACL,GAAG,EAAE,MAAM;QACX,IAAI,EAAE,MAAM;QACZ,GAAG,EAAE,MAAM;QACX,KAAK,EAAE,MAAM;QACb,MAAM,EAAE,MAAM;KACf,CAAA;AACH,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Payload embedded in the CSRF state token passed to GitHub during the
3
+ * App installation flow. Stateless — no DB storage needed.
4
+ */
5
+ export interface StatePayload {
6
+ /** Project to associate the installation with. */
7
+ projectId: string;
8
+ /** User who initiated the connection — must still be admin at callback time. */
9
+ userId: string;
10
+ /** 32-byte hex nonce for replay resistance within the 10-minute window. */
11
+ nonce: string;
12
+ mode: 'dashboard' | 'cli';
13
+ /** CLI-only: local HTTP server port for receiving credentials. */
14
+ port?: number;
15
+ /** Unix ms expiry — 10-minute window. */
16
+ expiresAt: number;
17
+ }
18
+ export type StateVerifyResult = {
19
+ valid: true;
20
+ payload: StatePayload;
21
+ } | {
22
+ valid: false;
23
+ reason: 'invalid_signature' | 'expired' | 'malformed';
24
+ };
25
+ /**
26
+ * Signs a CSRF state token using HMAC-SHA256.
27
+ * Format: `<base64url(payload)>.<base64url(signature)>`
28
+ *
29
+ * ```ts
30
+ * const state = signState({ projectId, userId, mode: 'dashboard' }, callbackSecret)
31
+ * const url = `https://github.com/apps/airdraft/installations/new?state=${state}`
32
+ * ```
33
+ */
34
+ export declare function signState(payload: Omit<StatePayload, 'nonce' | 'expiresAt'>, secret: string): string;
35
+ /**
36
+ * Verifies a CSRF state token. Returns the decoded payload on success,
37
+ * or a typed failure reason on any error.
38
+ */
39
+ export declare function verifyState(token: string, secret: string): StateVerifyResult;
40
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,WAAW,GAAG,KAAK,CAAA;IACzB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,GAAG,WAAW,CAAA;CAAE,CAAA;AAQ3E;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,WAAW,CAAC,EAClD,MAAM,EAAE,MAAM,GACb,MAAM,CASR;AAMD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CA+B5E"}
package/dist/state.js ADDED
@@ -0,0 +1,58 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
3
+ // ---------------------------------------------------------------------------
4
+ // Sign
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Signs a CSRF state token using HMAC-SHA256.
8
+ * Format: `<base64url(payload)>.<base64url(signature)>`
9
+ *
10
+ * ```ts
11
+ * const state = signState({ projectId, userId, mode: 'dashboard' }, callbackSecret)
12
+ * const url = `https://github.com/apps/airdraft/installations/new?state=${state}`
13
+ * ```
14
+ */
15
+ export function signState(payload, secret) {
16
+ const full = {
17
+ ...payload,
18
+ nonce: randomBytes(32).toString('hex'),
19
+ expiresAt: Date.now() + STATE_TTL_MS,
20
+ };
21
+ const encoded = Buffer.from(JSON.stringify(full)).toString('base64url');
22
+ const sig = createHmac('sha256', secret).update(encoded).digest('base64url');
23
+ return `${encoded}.${sig}`;
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Verify
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Verifies a CSRF state token. Returns the decoded payload on success,
30
+ * or a typed failure reason on any error.
31
+ */
32
+ export function verifyState(token, secret) {
33
+ const dotIdx = token.lastIndexOf('.');
34
+ if (dotIdx === -1)
35
+ return { valid: false, reason: 'malformed' };
36
+ const encoded = token.slice(0, dotIdx);
37
+ const receivedSig = token.slice(dotIdx + 1);
38
+ // Constant-time signature comparison
39
+ const expectedSig = createHmac('sha256', secret).update(encoded).digest('base64url');
40
+ const expectedBuf = Buffer.from(expectedSig);
41
+ const receivedBuf = Buffer.from(receivedSig);
42
+ if (expectedBuf.length !== receivedBuf.length ||
43
+ !timingSafeEqual(expectedBuf, receivedBuf)) {
44
+ return { valid: false, reason: 'invalid_signature' };
45
+ }
46
+ let payload;
47
+ try {
48
+ payload = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
49
+ }
50
+ catch {
51
+ return { valid: false, reason: 'malformed' };
52
+ }
53
+ if (Date.now() > payload.expiresAt) {
54
+ return { valid: false, reason: 'expired' };
55
+ }
56
+ return { valid: true, payload };
57
+ }
58
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AA4BtE,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,aAAa;AAEjD,8EAA8E;AAC9E,OAAO;AACP,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CACvB,OAAkD,EAClD,MAAc;IAEd,MAAM,IAAI,GAAiB;QACzB,GAAG,OAAO;QACV,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QACtC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY;KACrC,CAAA;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IACvE,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IAC5E,OAAO,GAAG,OAAO,IAAI,GAAG,EAAE,CAAA;AAC5B,CAAC;AAED,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,MAAc;IACvD,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IACrC,IAAI,MAAM,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAE/D,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACtC,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAE3C,qCAAqC;IACrC,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACpF,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC5C,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAE5C,IACE,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM;QACzC,CAAC,eAAe,CAAC,WAAW,EAAE,WAAW,CAAC,EAC1C,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAA;IACtD,CAAC;IAED,IAAI,OAAqB,CAAA;IACzB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAiB,CAAA;IAC1F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAC9C,CAAC;IAED,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;QACnC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAC5C,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AACjC,CAAC"}
package/dist/team.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { CloudAuthAdapter } from '@airdraft/cloud-auth';
2
+ import type { CloudDb } from './types.js';
3
+ /**
4
+ * GET /v1/teams
5
+ *
6
+ * Returns all teams the authenticated user is a member of.
7
+ */
8
+ export declare function handleListTeams(req: Request, db: CloudDb, auth: CloudAuthAdapter): Promise<Response>;
9
+ /**
10
+ * POST /v1/teams
11
+ *
12
+ * Body: `{ name: string }`
13
+ *
14
+ * Creates a new team and adds the creator as `owner`.
15
+ */
16
+ export declare function handleCreateTeam(req: Request, db: CloudDb, auth: CloudAuthAdapter): Promise<Response>;
17
+ /**
18
+ * GET /v1/teams/:teamSlug
19
+ *
20
+ * Returns team by slug. Requires membership.
21
+ */
22
+ export declare function handleGetTeam(req: Request, db: CloudDb, auth: CloudAuthAdapter, teamSlug: string): Promise<Response>;
23
+ /**
24
+ * PATCH /v1/teams/:teamSlug
25
+ *
26
+ * Body: `{ name?: string }`
27
+ *
28
+ * Updates team metadata. Requires `owner` or `admin` role.
29
+ */
30
+ export declare function handleUpdateTeam(req: Request, db: CloudDb, auth: CloudAuthAdapter, teamSlug: string): Promise<Response>;
31
+ //# sourceMappingURL=team.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../src/team.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAqBzC;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,QAAQ,CAAC,CAanB;AAMD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,QAAQ,CAAC,CA6CnB;AAMD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CAcnB;AAMD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CAkCnB"}
package/dist/team.js ADDED
@@ -0,0 +1,150 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Helpers
3
+ // ---------------------------------------------------------------------------
4
+ function json(body, status = 200) {
5
+ return new Response(JSON.stringify(body), {
6
+ status,
7
+ headers: { 'Content-Type': 'application/json' },
8
+ });
9
+ }
10
+ function err(code, message, status) {
11
+ return json({ error: { code, message } }, status);
12
+ }
13
+ // ---------------------------------------------------------------------------
14
+ // handleListTeams
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * GET /v1/teams
18
+ *
19
+ * Returns all teams the authenticated user is a member of.
20
+ */
21
+ export async function handleListTeams(req, db, auth) {
22
+ const identity = await auth.verifySession(req);
23
+ if (!identity)
24
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
25
+ const memberships = await db.memberships.find({ userId: identity.userId }).toArray();
26
+ const teamIds = memberships.map((m) => m.teamId);
27
+ const { ObjectId } = await import('mongodb');
28
+ const teams = await db.teams
29
+ .find({ _id: { $in: teamIds.map((id) => new ObjectId(id)) } })
30
+ .toArray();
31
+ return json({ data: teams });
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // handleCreateTeam
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * POST /v1/teams
38
+ *
39
+ * Body: `{ name: string }`
40
+ *
41
+ * Creates a new team and adds the creator as `owner`.
42
+ */
43
+ export async function handleCreateTeam(req, db, auth) {
44
+ const identity = await auth.verifySession(req);
45
+ if (!identity)
46
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
47
+ let body;
48
+ try {
49
+ body = await req.json();
50
+ }
51
+ catch {
52
+ return err('INVALID_BODY', 'Request body must be JSON', 400);
53
+ }
54
+ const name = typeof body.name === 'string' ? body.name.trim() : '';
55
+ if (!name)
56
+ return err('INVALID_NAME', 'Team name is required', 400);
57
+ // Generate URL-safe slug from name
58
+ const baseSlug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
59
+ if (!baseSlug)
60
+ return err('INVALID_NAME', 'Team name must contain alphanumeric characters', 400);
61
+ // Ensure slug uniqueness
62
+ const existing = await db.teams.findOne({ slug: baseSlug });
63
+ const slug = existing ? `${baseSlug}-${Date.now().toString(36)}` : baseSlug;
64
+ const now = new Date();
65
+ const { ObjectId } = await import('mongodb');
66
+ const teamId = new ObjectId();
67
+ await db.teams.insertOne({
68
+ _id: teamId,
69
+ name,
70
+ slug,
71
+ ownerId: identity.userId,
72
+ planSlug: 'free',
73
+ createdAt: now,
74
+ });
75
+ await db.memberships.insertOne({
76
+ _id: new ObjectId(),
77
+ teamId: teamId.toString(),
78
+ userId: identity.userId,
79
+ role: 'owner',
80
+ createdAt: now,
81
+ });
82
+ const team = await db.teams.findOne({ _id: teamId });
83
+ return json({ data: team }, 201);
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // handleGetTeam
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * GET /v1/teams/:teamSlug
90
+ *
91
+ * Returns team by slug. Requires membership.
92
+ */
93
+ export async function handleGetTeam(req, db, auth, teamSlug) {
94
+ const identity = await auth.verifySession(req);
95
+ if (!identity)
96
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
97
+ const team = await db.teams.findOne({ slug: teamSlug });
98
+ if (!team)
99
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
100
+ const membership = await db.memberships.findOne({
101
+ teamId: team._id.toString(),
102
+ userId: identity.userId,
103
+ });
104
+ if (!membership)
105
+ return err('FORBIDDEN', 'Not a member of this team', 403);
106
+ return json({ data: team });
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // handleUpdateTeam
110
+ // ---------------------------------------------------------------------------
111
+ /**
112
+ * PATCH /v1/teams/:teamSlug
113
+ *
114
+ * Body: `{ name?: string }`
115
+ *
116
+ * Updates team metadata. Requires `owner` or `admin` role.
117
+ */
118
+ export async function handleUpdateTeam(req, db, auth, teamSlug) {
119
+ const identity = await auth.verifySession(req);
120
+ if (!identity)
121
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
122
+ const team = await db.teams.findOne({ slug: teamSlug });
123
+ if (!team)
124
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
125
+ const membership = await db.memberships.findOne({
126
+ teamId: team._id.toString(),
127
+ userId: identity.userId,
128
+ });
129
+ if (!membership || !['owner', 'admin'].includes(membership.role)) {
130
+ return err('FORBIDDEN', 'Requires owner or admin role', 403);
131
+ }
132
+ let body;
133
+ try {
134
+ body = await req.json();
135
+ }
136
+ catch {
137
+ return err('INVALID_BODY', 'Request body must be JSON', 400);
138
+ }
139
+ const updates = {};
140
+ if (typeof body.name === 'string' && body.name.trim()) {
141
+ updates.name = body.name.trim();
142
+ }
143
+ if (Object.keys(updates).length === 0) {
144
+ return err('NO_CHANGES', 'No valid fields to update', 400);
145
+ }
146
+ await db.teams.updateOne({ _id: team._id }, { $set: updates });
147
+ const updated = await db.teams.findOne({ _id: team._id });
148
+ return json({ data: updated });
149
+ }
150
+ //# sourceMappingURL=team.js.map