@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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@airdraft/cloud` will be documented here.
4
+
5
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
@@ -0,0 +1,72 @@
1
+ import type { CloudAuthAdapter } from '@airdraft/cloud-auth';
2
+ import type { CloudDb, TokenCache } from './types.js';
3
+ /**
4
+ * Minimal @untools/pay client interface used by billing handlers.
5
+ * The concrete implementation is provided by the cloud app via `lib/pay.ts`.
6
+ */
7
+ export interface PayClient {
8
+ checkout: {
9
+ create(opts: {
10
+ amount: number;
11
+ currency: string;
12
+ provider: string;
13
+ metadata?: Record<string, unknown>;
14
+ }): Promise<{
15
+ url: string;
16
+ reference: string;
17
+ }>;
18
+ };
19
+ webhook: {
20
+ verify(req: Request, provider: string): Promise<{
21
+ verified: boolean;
22
+ data: Record<string, unknown>;
23
+ }>;
24
+ };
25
+ }
26
+ /**
27
+ * Resolves a team's plan (Redis-cached, 5-min TTL) and checks a named limit.
28
+ *
29
+ * Returns `{ allowed: boolean; current: number; max: number }`.
30
+ * `max: -1` means unlimited.
31
+ */
32
+ export declare function checkLimit(db: CloudDb, teamId: string, key: string, current: number, cache?: TokenCache): Promise<{
33
+ allowed: boolean;
34
+ current: number;
35
+ max: number;
36
+ }>;
37
+ /**
38
+ * Resolves a team's plan and returns the value of a named feature flag.
39
+ * Returns `false` if the plan or feature is not found.
40
+ */
41
+ export declare function checkFeature(db: CloudDb, teamId: string, key: string, cache?: TokenCache): Promise<boolean | number>;
42
+ /**
43
+ * GET /v1/billing/plans
44
+ *
45
+ * Returns all active plan definitions. Public — no auth required.
46
+ */
47
+ export declare function handleGetPlans(req: Request, db: CloudDb): Promise<Response>;
48
+ /**
49
+ * POST /v1/billing/upgrade
50
+ *
51
+ * Body: `{ planSlug: string; provider: string }`
52
+ *
53
+ * Creates a @untools/pay checkout session for the requested plan upgrade.
54
+ * Returns `{ checkoutUrl, reference }`.
55
+ * Requires `owner` role.
56
+ */
57
+ export declare function handleUpgradePlan(req: Request, db: CloudDb, auth: CloudAuthAdapter, pay: PayClient, teamSlug: string): Promise<Response>;
58
+ /**
59
+ * POST /v1/billing/webhooks/:provider
60
+ *
61
+ * Verifies the provider webhook signature and fulfills plan upgrade.
62
+ * Updates `TeamDoc.planSlug` on successful payment.
63
+ */
64
+ export declare function handleWebhookSettlement(req: Request, provider: 'paystack' | 'flutterwave' | '100pay', db: CloudDb, pay: PayClient): Promise<Response>;
65
+ /**
66
+ * GET /v1/teams/:teamSlug/usage
67
+ *
68
+ * Returns current usage stats and plan limits for the team.
69
+ * Requires membership.
70
+ */
71
+ export declare function handleGetUsage(req: Request, db: CloudDb, auth: CloudAuthAdapter, teamSlug: string): Promise<Response>;
72
+ //# sourceMappingURL=billing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAW,UAAU,EAAE,MAAM,YAAY,CAAA;AAM9D;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE;QACR,MAAM,CAAC,IAAI,EAAE;YACX,MAAM,EAAE,MAAM,CAAA;YACd,QAAQ,EAAE,MAAM,CAAA;YAChB,QAAQ,EAAE,MAAM,CAAA;YAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SACnC,GAAG,OAAO,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAChD,CAAA;IACD,OAAO,EAAE;QACP,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;YAAE,QAAQ,EAAE,OAAO,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC,CAAA;KACtG,CAAA;CACF;AA2BD;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAK7D;AAMD;;;GAGG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,UAAU,GACjB,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,CAG3B;AAuCD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CAInB;AAMD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,GAAG,EAAE,SAAS,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CA4CnB;AAMD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,UAAU,GAAG,aAAa,GAAG,QAAQ,EAC/C,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,QAAQ,CAAC,CAyBnB;AAMD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CA8BnB"}
@@ -0,0 +1,204 @@
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
+ const PLAN_CACHE_TTL_S = 5 * 60; // 5 minutes
14
+ function planCacheKey(teamId) {
15
+ return `plan:${teamId}`;
16
+ }
17
+ // ---------------------------------------------------------------------------
18
+ // checkLimit
19
+ // ---------------------------------------------------------------------------
20
+ /**
21
+ * Resolves a team's plan (Redis-cached, 5-min TTL) and checks a named limit.
22
+ *
23
+ * Returns `{ allowed: boolean; current: number; max: number }`.
24
+ * `max: -1` means unlimited.
25
+ */
26
+ export async function checkLimit(db, teamId, key, current, cache) {
27
+ const plan = await resolvePlan(db, teamId, cache);
28
+ const max = plan?.limits[key] ?? -1;
29
+ if (max === -1)
30
+ return { allowed: true, current, max };
31
+ return { allowed: current < max, current, max };
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // checkFeature
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Resolves a team's plan and returns the value of a named feature flag.
38
+ * Returns `false` if the plan or feature is not found.
39
+ */
40
+ export async function checkFeature(db, teamId, key, cache) {
41
+ const plan = await resolvePlan(db, teamId, cache);
42
+ return plan?.features[key] ?? false;
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // resolvePlan (internal)
46
+ // ---------------------------------------------------------------------------
47
+ async function resolvePlan(db, teamId, cache) {
48
+ if (cache) {
49
+ const cached = await cache.get(planCacheKey(teamId));
50
+ if (cached) {
51
+ try {
52
+ return JSON.parse(cached);
53
+ }
54
+ catch {
55
+ // invalid cache — fall through to DB
56
+ }
57
+ }
58
+ }
59
+ const { ObjectId } = await import('mongodb');
60
+ const team = await db.teams.findOne({ _id: new ObjectId(teamId) });
61
+ if (!team)
62
+ return null;
63
+ const plan = await db.plans.findOne({ slug: team.planSlug, isActive: true });
64
+ if (cache && plan) {
65
+ await cache.set(planCacheKey(teamId), JSON.stringify(plan), PLAN_CACHE_TTL_S);
66
+ }
67
+ return plan;
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // handleGetPlans
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * GET /v1/billing/plans
74
+ *
75
+ * Returns all active plan definitions. Public — no auth required.
76
+ */
77
+ export async function handleGetPlans(req, db) {
78
+ void req;
79
+ const plans = await db.plans.find({ isActive: true }).toArray();
80
+ return json({ data: plans });
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // handleUpgradePlan
84
+ // ---------------------------------------------------------------------------
85
+ /**
86
+ * POST /v1/billing/upgrade
87
+ *
88
+ * Body: `{ planSlug: string; provider: string }`
89
+ *
90
+ * Creates a @untools/pay checkout session for the requested plan upgrade.
91
+ * Returns `{ checkoutUrl, reference }`.
92
+ * Requires `owner` role.
93
+ */
94
+ export async function handleUpgradePlan(req, db, auth, pay, teamSlug) {
95
+ const identity = await auth.verifySession(req);
96
+ if (!identity)
97
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
98
+ const team = await db.teams.findOne({ slug: teamSlug });
99
+ if (!team)
100
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
101
+ const membership = await db.memberships.findOne({
102
+ teamId: team._id.toString(),
103
+ userId: identity.userId,
104
+ });
105
+ if (!membership || membership.role !== 'owner') {
106
+ return err('FORBIDDEN', 'Requires owner role', 403);
107
+ }
108
+ let body;
109
+ try {
110
+ body = await req.json();
111
+ }
112
+ catch {
113
+ return err('INVALID_BODY', 'Request body must be JSON', 400);
114
+ }
115
+ const planSlug = typeof body.planSlug === 'string' ? body.planSlug : '';
116
+ if (!planSlug)
117
+ return err('INVALID_PLAN', 'planSlug is required', 400);
118
+ const provider = typeof body.provider === 'string' ? body.provider : '';
119
+ if (!provider)
120
+ return err('INVALID_PROVIDER', 'provider is required', 400);
121
+ const plan = await db.plans.findOne({ slug: planSlug, isActive: true });
122
+ if (!plan)
123
+ return err('PLAN_NOT_FOUND', 'Plan not found', 404);
124
+ const session = await pay.checkout.create({
125
+ amount: plan.priceMonthly,
126
+ currency: plan.currency,
127
+ provider,
128
+ metadata: {
129
+ teamId: team._id.toString(),
130
+ teamSlug: team.slug,
131
+ planSlug,
132
+ userId: identity.userId,
133
+ },
134
+ });
135
+ return json({ data: { checkoutUrl: session.url, reference: session.reference } });
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // handleWebhookSettlement
139
+ // ---------------------------------------------------------------------------
140
+ /**
141
+ * POST /v1/billing/webhooks/:provider
142
+ *
143
+ * Verifies the provider webhook signature and fulfills plan upgrade.
144
+ * Updates `TeamDoc.planSlug` on successful payment.
145
+ */
146
+ export async function handleWebhookSettlement(req, provider, db, pay) {
147
+ const { verified, data } = await pay.webhook.verify(req, provider);
148
+ if (!verified)
149
+ return err('INVALID_SIGNATURE', 'Webhook signature verification failed', 400);
150
+ const status = data['status'];
151
+ if (status !== 'success' && status !== 'successful') {
152
+ // Non-success event — acknowledge without action
153
+ return new Response(null, { status: 200 });
154
+ }
155
+ const metadata = (data['metadata'] ?? {});
156
+ const teamId = typeof metadata['teamId'] === 'string' ? metadata['teamId'] : null;
157
+ const planSlug = typeof metadata['planSlug'] === 'string' ? metadata['planSlug'] : null;
158
+ if (!teamId || !planSlug) {
159
+ return err('MISSING_METADATA', 'Webhook payload missing teamId or planSlug', 400);
160
+ }
161
+ const { ObjectId } = await import('mongodb');
162
+ await db.teams.updateOne({ _id: new ObjectId(teamId) }, { $set: { planSlug } });
163
+ return new Response(null, { status: 200 });
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // handleGetUsage
167
+ // ---------------------------------------------------------------------------
168
+ /**
169
+ * GET /v1/teams/:teamSlug/usage
170
+ *
171
+ * Returns current usage stats and plan limits for the team.
172
+ * Requires membership.
173
+ */
174
+ export async function handleGetUsage(req, db, auth, teamSlug) {
175
+ const identity = await auth.verifySession(req);
176
+ if (!identity)
177
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
178
+ const team = await db.teams.findOne({ slug: teamSlug });
179
+ if (!team)
180
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
181
+ const membership = await db.memberships.findOne({
182
+ teamId: team._id.toString(),
183
+ userId: identity.userId,
184
+ });
185
+ if (!membership)
186
+ return err('FORBIDDEN', 'Not a member of this team', 403);
187
+ const teamId = team._id.toString();
188
+ const [projectCount, memberCount, plan] = await Promise.all([
189
+ db.projects.countDocuments({ teamId }),
190
+ db.memberships.countDocuments({ teamId }),
191
+ db.plans.findOne({ slug: team.planSlug, isActive: true }),
192
+ ]);
193
+ return json({
194
+ data: {
195
+ planSlug: team.planSlug,
196
+ plan: plan ?? null,
197
+ usage: {
198
+ projects: projectCount,
199
+ members: memberCount,
200
+ },
201
+ },
202
+ });
203
+ }
204
+ //# sourceMappingURL=billing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAyBA,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,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,SAAS,GAAG,CAAC,IAAY,EAAE,OAAe,EAAE,MAAc;IACxD,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;AACnD,CAAC;AAED,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,CAAA,CAAC,YAAY;AAE5C,SAAS,YAAY,CAAC,MAAc;IAClC,OAAO,QAAQ,MAAM,EAAE,CAAA;AACzB,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,EAAW,EACX,MAAc,EACd,GAAW,EACX,OAAe,EACf,KAAkB;IAElB,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;IACjD,MAAM,GAAG,GAAG,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;IACnC,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAA;IACtD,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAA;AACjD,CAAC;AAED,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,EAAW,EACX,MAAc,EACd,GAAW,EACX,KAAkB;IAElB,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;IACjD,OAAO,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAA;AACrC,CAAC;AAED,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E,KAAK,UAAU,WAAW,CACxB,EAAW,EACX,MAAc,EACd,KAAkB;IAElB,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAA;QACpD,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAY,CAAA;YACtC,CAAC;YAAC,MAAM,CAAC;gBACP,qCAAqC;YACvC,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAClE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IAEtB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAE5E,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,MAAM,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,gBAAgB,CAAC,CAAA;IAC/E,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAY,EACZ,EAAW;IAEX,KAAK,GAAG,CAAA;IACR,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAA;IAC/D,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;AAC9B,CAAC;AAED,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAY,EACZ,EAAW,EACX,IAAsB,EACtB,GAAc,EACd,QAAgB;IAEhB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;IAC9C,IAAI,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,cAAc,EAAE,mBAAmB,EAAE,GAAG,CAAC,CAAA;IAEnE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;IACvD,IAAI,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC,gBAAgB,EAAE,gBAAgB,EAAE,GAAG,CAAC,CAAA;IAE9D,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC;QAC9C,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE;QAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM;KACxB,CAAC,CAAA;IACF,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC/C,OAAO,GAAG,CAAC,WAAW,EAAE,qBAAqB,EAAE,GAAG,CAAC,CAAA;IACrD,CAAC;IAED,IAAI,IAAgD,CAAA;IACpD,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC,cAAc,EAAE,2BAA2B,EAAE,GAAG,CAAC,CAAA;IAC9D,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IACvE,IAAI,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,cAAc,EAAE,sBAAsB,EAAE,GAAG,CAAC,CAAA;IAEtE,MAAM,QAAQ,GAAG,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IACvE,IAAI,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,kBAAkB,EAAE,sBAAsB,EAAE,GAAG,CAAC,CAAA;IAE1E,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACvE,IAAI,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC,gBAAgB,EAAE,gBAAgB,EAAE,GAAG,CAAC,CAAA;IAE9D,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QACxC,MAAM,EAAE,IAAI,CAAC,YAAY;QACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ;QACR,QAAQ,EAAE;YACR,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE;YAC3B,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ;YACR,MAAM,EAAE,QAAQ,CAAC,MAAM;SACxB;KACF,CAAC,CAAA;IAEF,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;AACnF,CAAC;AAED,8EAA8E;AAC9E,0BAA0B;AAC1B,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,GAAY,EACZ,QAA+C,EAC/C,EAAW,EACX,GAAc;IAEd,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IAClE,IAAI,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,mBAAmB,EAAE,uCAAuC,EAAE,GAAG,CAAC,CAAA;IAE5F,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC7B,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;QACpD,iDAAiD;QACjD,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC5C,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAA4B,CAAA;IACpE,MAAM,MAAM,GAAG,OAAO,QAAQ,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACjF,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,UAAU,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAEvF,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzB,OAAO,GAAG,CAAC,kBAAkB,EAAE,4CAA4C,EAAE,GAAG,CAAC,CAAA;IACnF,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,CACtB,EAAE,GAAG,EAAE,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,EAC7B,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,CACvB,CAAA;IAED,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;AAC5C,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAY,EACZ,EAAW,EACX,IAAsB,EACtB,QAAgB;IAEhB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;IAC9C,IAAI,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,cAAc,EAAE,mBAAmB,EAAE,GAAG,CAAC,CAAA;IAEnE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;IACvD,IAAI,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC,gBAAgB,EAAE,gBAAgB,EAAE,GAAG,CAAC,CAAA;IAE9D,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC;QAC9C,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE;QAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM;KACxB,CAAC,CAAA;IACF,IAAI,CAAC,UAAU;QAAE,OAAO,GAAG,CAAC,WAAW,EAAE,2BAA2B,EAAE,GAAG,CAAC,CAAA;IAE1E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;IAClC,MAAM,CAAC,YAAY,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC1D,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QACtC,EAAE,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QACzC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;KAC1D,CAAC,CAAA;IAEF,OAAO,IAAI,CAAC;QACV,IAAI,EAAE;YACJ,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,IAAI,EAAE,IAAI,IAAI,IAAI;YAClB,KAAK,EAAE;gBACL,QAAQ,EAAE,YAAY;gBACtB,OAAO,EAAE,WAAW;aACrB;SACF;KACF,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { CloudDb } from './types.js';
2
+ export interface CallbackOptions {
3
+ /** `AIRDRAFT_CALLBACK_SECRET` env var — used to verify the CSRF state token. */
4
+ callbackSecret: string;
5
+ /** GitHub App numeric ID — `GITHUB_APP_ID` env var. */
6
+ githubAppId: string;
7
+ /** GitHub App PEM private key — `GITHUB_APP_PRIVATE_KEY` env var. */
8
+ githubAppPrivateKey: string;
9
+ db: CloudDb;
10
+ /**
11
+ * Base URL for the cloud dashboard (e.g. `https://app.airdraft.space`).
12
+ * Used to build post-connection redirect URLs.
13
+ */
14
+ dashboardBaseUrl: string;
15
+ }
16
+ /**
17
+ * Handler for `GET /github/callback`.
18
+ *
19
+ * Processes the GitHub App installation callback after a user installs or
20
+ * updates the Airdraft GitHub App. Validates the CSRF state token, verifies
21
+ * the installation, writes the project connection, and issues a project API key.
22
+ *
23
+ * **Single repo granted:** auto-selects, completes connection immediately.
24
+ * **Multiple repos granted:** redirects to the dashboard repo picker step.
25
+ *
26
+ * ```ts
27
+ * export async function GET(req: Request) {
28
+ * return handleCallback(req, {
29
+ * callbackSecret: process.env.AIRDRAFT_CALLBACK_SECRET!,
30
+ * githubAppId: process.env.GITHUB_APP_ID!,
31
+ * githubAppPrivateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
32
+ * db,
33
+ * dashboardBaseUrl: 'https://app.airdraft.space',
34
+ * })
35
+ * }
36
+ * ```
37
+ */
38
+ export declare function handleCallback(req: Request, opts: CallbackOptions): Promise<Response>;
39
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../src/callback.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAMzC,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,cAAc,EAAE,MAAM,CAAA;IACtB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,mBAAmB,EAAE,MAAM,CAAA;IAC3B,EAAE,EAAE,OAAO,CAAA;IACX;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAyDD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,QAAQ,CAAC,CA8InB"}
@@ -0,0 +1,178 @@
1
+ import { generateApiKey } from '@airdraft/auth';
2
+ import { signGitHubAppJwt } from './jwt.js';
3
+ import { verifyState } from './state.js';
4
+ async function getInstallationToken(installationId, appId, privateKey) {
5
+ const jwt = await signGitHubAppJwt(appId, privateKey);
6
+ const res = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, {
7
+ method: 'POST',
8
+ headers: {
9
+ Authorization: `Bearer ${jwt}`,
10
+ Accept: 'application/vnd.github+json',
11
+ 'X-GitHub-Api-Version': '2022-11-28',
12
+ },
13
+ });
14
+ if (!res.ok)
15
+ throw new Error(`GitHub token API returned ${res.status}`);
16
+ const body = await res.json();
17
+ return body.token;
18
+ }
19
+ async function listInstallationRepos(installationToken) {
20
+ const res = await fetch('https://api.github.com/installation/repositories?per_page=100', {
21
+ headers: {
22
+ Authorization: `Bearer ${installationToken}`,
23
+ Accept: 'application/vnd.github+json',
24
+ 'X-GitHub-Api-Version': '2022-11-28',
25
+ },
26
+ });
27
+ if (!res.ok)
28
+ throw new Error(`GitHub repos API returned ${res.status}`);
29
+ const body = await res.json();
30
+ return body.repositories;
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // handleCallback
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * Handler for `GET /github/callback`.
37
+ *
38
+ * Processes the GitHub App installation callback after a user installs or
39
+ * updates the Airdraft GitHub App. Validates the CSRF state token, verifies
40
+ * the installation, writes the project connection, and issues a project API key.
41
+ *
42
+ * **Single repo granted:** auto-selects, completes connection immediately.
43
+ * **Multiple repos granted:** redirects to the dashboard repo picker step.
44
+ *
45
+ * ```ts
46
+ * export async function GET(req: Request) {
47
+ * return handleCallback(req, {
48
+ * callbackSecret: process.env.AIRDRAFT_CALLBACK_SECRET!,
49
+ * githubAppId: process.env.GITHUB_APP_ID!,
50
+ * githubAppPrivateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
51
+ * db,
52
+ * dashboardBaseUrl: 'https://app.airdraft.space',
53
+ * })
54
+ * }
55
+ * ```
56
+ */
57
+ export async function handleCallback(req, opts) {
58
+ const url = new URL(req.url);
59
+ const installationIdStr = url.searchParams.get('installation_id');
60
+ const setupAction = url.searchParams.get('setup_action');
61
+ const stateToken = url.searchParams.get('state');
62
+ // -------------------------------------------------------------------------
63
+ // Unmatched installation — no state token (e.g. GitHub Marketplace)
64
+ // -------------------------------------------------------------------------
65
+ if (!stateToken) {
66
+ if (installationIdStr) {
67
+ const connectUrl = `${opts.dashboardBaseUrl}/connect?installation_id=${installationIdStr}`;
68
+ return Response.redirect(connectUrl, 302);
69
+ }
70
+ return new Response('Bad Request: missing state', { status: 400 });
71
+ }
72
+ // -------------------------------------------------------------------------
73
+ // 1. Verify CSRF state token
74
+ // -------------------------------------------------------------------------
75
+ const stateResult = verifyState(stateToken, opts.callbackSecret);
76
+ if (!stateResult.valid) {
77
+ const message = stateResult.reason === 'expired'
78
+ ? 'Link expired — please try connecting again.'
79
+ : 'Invalid state token.';
80
+ return new Response(message, { status: 400 });
81
+ }
82
+ const { projectId, userId, mode, port } = stateResult.payload;
83
+ const installationId = parseInt(installationIdStr ?? '', 10);
84
+ if (isNaN(installationId)) {
85
+ return new Response('Bad Request: missing installation_id', { status: 400 });
86
+ }
87
+ // -------------------------------------------------------------------------
88
+ // 2. Load and validate project
89
+ // -------------------------------------------------------------------------
90
+ const { ObjectId } = await import('mongodb');
91
+ let projectOid;
92
+ try {
93
+ projectOid = new ObjectId(projectId);
94
+ }
95
+ catch {
96
+ return new Response('Bad Request: invalid projectId', { status: 400 });
97
+ }
98
+ const project = await opts.db.projects.findOne({ _id: projectOid });
99
+ if (!project)
100
+ return new Response('Not Found: project not found', { status: 404 });
101
+ const team = await opts.db.teams.findOne({ _id: new ObjectId(project.teamId) });
102
+ if (!team)
103
+ return new Response('Not Found: team not found', { status: 404 });
104
+ // -------------------------------------------------------------------------
105
+ // 3. Verify userId is still admin/owner of the project's team
106
+ // -------------------------------------------------------------------------
107
+ const membership = await opts.db.memberships.findOne({
108
+ teamId: project.teamId,
109
+ userId,
110
+ role: { $in: ['owner', 'admin'] },
111
+ });
112
+ if (!membership) {
113
+ return new Response('Forbidden: insufficient permissions', { status: 403 });
114
+ }
115
+ // -------------------------------------------------------------------------
116
+ // 4. Verify installation via GitHub API + get accessible repos
117
+ // -------------------------------------------------------------------------
118
+ let installationToken;
119
+ let repos;
120
+ try {
121
+ installationToken = await getInstallationToken(installationId, opts.githubAppId, opts.githubAppPrivateKey);
122
+ repos = await listInstallationRepos(installationToken);
123
+ }
124
+ catch (err) {
125
+ const msg = err instanceof Error ? err.message : String(err);
126
+ return new Response(`GitHub App installation could not be verified: ${msg}`, { status: 502 });
127
+ }
128
+ if (repos.length === 0) {
129
+ return new Response('No repositories accessible. Please grant access to at least one repository.', {
130
+ status: 400,
131
+ });
132
+ }
133
+ // -------------------------------------------------------------------------
134
+ // 5. Repo selection
135
+ // -------------------------------------------------------------------------
136
+ if (repos.length > 1) {
137
+ // Redirect to dashboard repo picker — connection is not written yet
138
+ const pickerUrl = `${opts.dashboardBaseUrl}/${team.slug}/${project.slug}/settings/github?installation_id=${installationId}&step=pick-repo`;
139
+ return Response.redirect(pickerUrl, 302);
140
+ }
141
+ // Single repo — auto-select
142
+ const selectedRepo = repos[0];
143
+ // -------------------------------------------------------------------------
144
+ // 6. Write project.github + generate / rotate project API key
145
+ // -------------------------------------------------------------------------
146
+ const { key: apiKey, hash: apiKeyHash } = generateApiKey();
147
+ await opts.db.projects.updateOne({ _id: projectOid }, {
148
+ $set: {
149
+ github: {
150
+ installationId,
151
+ repo: selectedRepo.full_name,
152
+ branch: selectedRepo.default_branch,
153
+ connectedAt: new Date(),
154
+ connectedBy: userId,
155
+ },
156
+ apiKeyHash,
157
+ updatedAt: new Date(),
158
+ },
159
+ });
160
+ // -------------------------------------------------------------------------
161
+ // 7. Redirect
162
+ // -------------------------------------------------------------------------
163
+ if (mode === 'cli' && port !== undefined) {
164
+ // CLI mode — redirect to local server with credentials
165
+ const cliCallbackUrl = new URL(`http://localhost:${port}/callback`);
166
+ cliCallbackUrl.searchParams.set('project_key', apiKey);
167
+ cliCallbackUrl.searchParams.set('repo', selectedRepo.full_name);
168
+ cliCallbackUrl.searchParams.set('branch', selectedRepo.default_branch);
169
+ return Response.redirect(cliCallbackUrl.toString(), 302);
170
+ }
171
+ // Dashboard mode — redirect to project settings with one-time key in query param
172
+ // (Dashboard shows the key once and removes it from the URL on load)
173
+ const successUrl = new URL(`/${team.slug}/${project.slug}/settings/github`, opts.dashboardBaseUrl);
174
+ successUrl.searchParams.set('connected', '1');
175
+ successUrl.searchParams.set('project_key', apiKey); // shown once, then discarded
176
+ return Response.redirect(successUrl.toString(), 302);
177
+ }
178
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../src/callback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAc,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAoCxC,KAAK,UAAU,oBAAoB,CACjC,cAAsB,EACtB,KAAa,EACb,UAAkB;IAElB,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;IACrD,MAAM,GAAG,GAAG,MAAM,KAAK,CACrB,4CAA4C,cAAc,gBAAgB,EAC1E;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,GAAG,EAAE;YAC9B,MAAM,EAAE,6BAA6B;YACrC,sBAAsB,EAAE,YAAY;SACrC;KACF,CACF,CAAA;IACD,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;IACvE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAuB,CAAA;IAClD,OAAO,IAAI,CAAC,KAAK,CAAA;AACnB,CAAC;AAED,KAAK,UAAU,qBAAqB,CAClC,iBAAyB;IAEzB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,+DAA+D,EAAE;QACvF,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,iBAAiB,EAAE;YAC5C,MAAM,EAAE,6BAA6B;YACrC,sBAAsB,EAAE,YAAY;SACrC;KACF,CAAC,CAAA;IACF,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;IACvE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAqC,CAAA;IAChE,OAAO,IAAI,CAAC,YAAY,CAAA;AAC1B,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAY,EACZ,IAAqB;IAErB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,MAAM,iBAAiB,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;IACjE,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;IACxD,MAAM,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAEhD,4EAA4E;IAC5E,oEAAoE;IACpE,4EAA4E;IAC5E,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,IAAI,iBAAiB,EAAE,CAAC;YACtB,MAAM,UAAU,GAAG,GAAG,IAAI,CAAC,gBAAgB,4BAA4B,iBAAiB,EAAE,CAAA;YAC1F,OAAO,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;QAC3C,CAAC;QACD,OAAO,IAAI,QAAQ,CAAC,4BAA4B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpE,CAAC;IAED,4EAA4E;IAC5E,6BAA6B;IAC7B,4EAA4E;IAC5E,MAAM,WAAW,GAAG,WAAW,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;IAChE,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACvB,MAAM,OAAO,GACX,WAAW,CAAC,MAAM,KAAK,SAAS;YAC9B,CAAC,CAAC,6CAA6C;YAC/C,CAAC,CAAC,sBAAsB,CAAA;QAC5B,OAAO,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC/C,CAAC;IAED,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,OAAO,CAAA;IAC7D,MAAM,cAAc,GAAG,QAAQ,CAAC,iBAAiB,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IAC5D,IAAI,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,QAAQ,CAAC,sCAAsC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC9E,CAAC;IAED,4EAA4E;IAC5E,+BAA+B;IAC/B,4EAA4E;IAC5E,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,UAAyC,CAAA;IAC7C,IAAI,CAAC;QACH,UAAU,GAAG,IAAI,QAAQ,CAAC,SAAS,CAAC,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,QAAQ,CAAC,gCAAgC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACxE,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAA;IACnE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,QAAQ,CAAC,8BAA8B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAElF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC/E,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,QAAQ,CAAC,2BAA2B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAE5E,4EAA4E;IAC5E,8DAA8D;IAC9D,4EAA4E;IAC5E,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC;QACnD,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM;QACN,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE;KAClC,CAAC,CAAA;IACF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,QAAQ,CAAC,qCAAqC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC7E,CAAC;IAED,4EAA4E;IAC5E,+DAA+D;IAC/D,4EAA4E;IAC5E,IAAI,iBAAyB,CAAA;IAC7B,IAAI,KAA+B,CAAA;IACnC,IAAI,CAAC;QACH,iBAAiB,GAAG,MAAM,oBAAoB,CAC5C,cAAc,EACd,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,mBAAmB,CACzB,CAAA;QACD,KAAK,GAAG,MAAM,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IACxD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5D,OAAO,IAAI,QAAQ,CAAC,kDAAkD,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC/F,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,IAAI,QAAQ,CAAC,6EAA6E,EAAE;YACjG,MAAM,EAAE,GAAG;SACZ,CAAC,CAAA;IACJ,CAAC;IAED,4EAA4E;IAC5E,oBAAoB;IACpB,4EAA4E;IAC5E,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,oEAAoE;QACpE,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,oCAAoC,cAAc,iBAAiB,CAAA;QAC1I,OAAO,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;IAC1C,CAAC;IAED,4BAA4B;IAC5B,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;IAE9B,4EAA4E;IAC5E,8DAA8D;IAC9D,4EAA4E;IAC5E,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,cAAc,EAAE,CAAA;IAE1D,MAAM,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAC9B,EAAE,GAAG,EAAE,UAAU,EAAE,EACnB;QACE,IAAI,EAAE;YACJ,MAAM,EAAE;gBACN,cAAc;gBACd,IAAI,EAAE,YAAY,CAAC,SAAS;gBAC5B,MAAM,EAAE,YAAY,CAAC,cAAc;gBACnC,WAAW,EAAE,IAAI,IAAI,EAAE;gBACvB,WAAW,EAAE,MAAM;aACpB;YACD,UAAU;YACV,SAAS,EAAE,IAAI,IAAI,EAAE;SACtB;KACF,CACF,CAAA;IAED,4EAA4E;IAC5E,cAAc;IACd,4EAA4E;IAC5E,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACzC,uDAAuD;QACvD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,oBAAoB,IAAI,WAAW,CAAC,CAAA;QACnE,cAAc,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;QACtD,cAAc,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,SAAS,CAAC,CAAA;QAC/D,cAAc,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,cAAc,CAAC,CAAA;QACtE,OAAO,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAA;IAC1D,CAAC;IAED,iFAAiF;IACjF,qEAAqE;IACrE,MAAM,UAAU,GAAG,IAAI,GAAG,CACxB,IAAI,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,kBAAkB,EAC/C,IAAI,CAAC,gBAAgB,CACtB,CAAA;IACD,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;IAC7C,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA,CAAC,6BAA6B;IAChF,OAAO,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAA;AACtD,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Signs a short-lived CLI JWT for the given userId.
3
+ *
4
+ * ```ts
5
+ * const token = await signCliToken(identity.userId, process.env.AIRDRAFT_CLI_SECRET!)
6
+ * ```
7
+ */
8
+ export declare function signCliToken(userId: string, secret: string): string;
9
+ /**
10
+ * Verifies a CLI JWT and returns the userId, or `null` if invalid/expired.
11
+ *
12
+ * ```ts
13
+ * const userId = verifyCliToken(token, process.env.AIRDRAFT_CLI_SECRET!)
14
+ * if (!userId) return Response.json({ error: { code: 'UNAUTHORIZED', message: 'Invalid CLI token' } }, { status: 401 })
15
+ * ```
16
+ */
17
+ export declare function verifyCliToken(token: string, secret: string): string | null;
18
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAgBA;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQnE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B3E"}
package/dist/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ // ---------------------------------------------------------------------------
3
+ // CLI JWT helpers — HS256 via Node.js crypto (no extra dependencies)
4
+ // ---------------------------------------------------------------------------
5
+ const CLI_TOKEN_TTL_S = 15 * 60; // 15 minutes
6
+ function b64url(input) {
7
+ return Buffer.from(input, 'utf8').toString('base64url');
8
+ }
9
+ function hmacSign(data, secret) {
10
+ return createHmac('sha256', secret).update(data).digest('base64url');
11
+ }
12
+ /**
13
+ * Signs a short-lived CLI JWT for the given userId.
14
+ *
15
+ * ```ts
16
+ * const token = await signCliToken(identity.userId, process.env.AIRDRAFT_CLI_SECRET!)
17
+ * ```
18
+ */
19
+ export function signCliToken(userId, secret) {
20
+ const now = Math.floor(Date.now() / 1000);
21
+ const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
22
+ const payload = b64url(JSON.stringify({ sub: userId, type: 'cli', iat: now, exp: now + CLI_TOKEN_TTL_S }));
23
+ const sig = hmacSign(`${header}.${payload}`, secret);
24
+ return `${header}.${payload}.${sig}`;
25
+ }
26
+ /**
27
+ * Verifies a CLI JWT and returns the userId, or `null` if invalid/expired.
28
+ *
29
+ * ```ts
30
+ * const userId = verifyCliToken(token, process.env.AIRDRAFT_CLI_SECRET!)
31
+ * if (!userId) return Response.json({ error: { code: 'UNAUTHORIZED', message: 'Invalid CLI token' } }, { status: 401 })
32
+ * ```
33
+ */
34
+ export function verifyCliToken(token, secret) {
35
+ const parts = token.split('.');
36
+ if (parts.length !== 3)
37
+ return null;
38
+ const [header, payload, sig] = parts;
39
+ const expected = hmacSign(`${header}.${payload}`, secret);
40
+ const expectedBuf = Buffer.from(expected, 'utf8');
41
+ const actualBuf = Buffer.from(sig, 'utf8');
42
+ if (expectedBuf.length !== actualBuf.length ||
43
+ !timingSafeEqual(expectedBuf, actualBuf)) {
44
+ return null;
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (parsed.type !== 'cli' || typeof parsed.sub !== 'string')
54
+ return null;
55
+ if (typeof parsed.exp === 'number' && parsed.exp < Math.floor(Date.now() / 1000))
56
+ return null;
57
+ return parsed.sub;
58
+ }
59
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEzD,8EAA8E;AAC9E,qEAAqE;AACrE,8EAA8E;AAE9E,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,CAAA,CAAC,aAAa;AAE7C,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AACzD,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc;IAC5C,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,MAAc;IACzD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACzC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IACnE,MAAM,OAAO,GAAG,MAAM,CACpB,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,eAAe,EAAE,CAAC,CACnF,CAAA;IACD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,MAAM,IAAI,OAAO,EAAE,EAAE,MAAM,CAAC,CAAA;IACpD,OAAO,GAAG,MAAM,IAAI,OAAO,IAAI,GAAG,EAAE,CAAA;AACtC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,MAAc;IAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,KAAK,CAAA;IAEpC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,MAAM,IAAI,OAAO,EAAE,EAAE,MAAM,CAAC,CAAA;IACzD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACjD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAC1C,IACE,WAAW,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;QACvC,CAAC,eAAe,CAAC,WAAW,EAAE,SAAS,CAAC,EACxC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,MAAwD,CAAA;IAC5D,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACxE,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAE7F,OAAO,MAAM,CAAC,GAAG,CAAA;AACnB,CAAC"}
package/dist/db.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { MongoClient } from 'mongodb';
2
+ import type { CloudDb } from './types.js';
3
+ /**
4
+ * Creates a typed `CloudDb` handle from a connected `MongoClient`.
5
+ *
6
+ * ```ts
7
+ * const client = new MongoClient(process.env.MONGODB_URI!)
8
+ * await client.connect()
9
+ * export const db = createCloudDb(client)
10
+ * ```
11
+ */
12
+ export declare function createCloudDb(client: MongoClient, dbName?: string): CloudDb;
13
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,KAAK,EAAE,OAAO,EAA0F,MAAM,YAAY,CAAA;AAEjI;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,SAAa,GAAG,OAAO,CAY/E"}