@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/db.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Creates a typed `CloudDb` handle from a connected `MongoClient`.
3
+ *
4
+ * ```ts
5
+ * const client = new MongoClient(process.env.MONGODB_URI!)
6
+ * await client.connect()
7
+ * export const db = createCloudDb(client)
8
+ * ```
9
+ */
10
+ export function createCloudDb(client, dbName = 'airdraft') {
11
+ const db = client.db(dbName);
12
+ return {
13
+ projects: db.collection('projects'),
14
+ teams: db.collection('teams'),
15
+ memberships: db.collection('memberships'),
16
+ invites: db.collection('invites'),
17
+ plans: db.collection('plans'),
18
+ customDomains: db.collection('customDomains'),
19
+ embedTokens: db.collection('embedTokens'),
20
+ auditEvents: db.collection('auditEvents'),
21
+ };
22
+ }
23
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,MAAmB,EAAE,MAAM,GAAG,UAAU;IACpE,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAA;IAC5B,OAAO;QACL,QAAQ,EAAE,EAAE,CAAC,UAAU,CAAa,UAAU,CAAC;QAC/C,KAAK,EAAE,EAAE,CAAC,UAAU,CAAU,OAAO,CAAC;QACtC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAgB,aAAa,CAAC;QACxD,OAAO,EAAE,EAAE,CAAC,UAAU,CAAY,SAAS,CAAC;QAC5C,KAAK,EAAE,EAAE,CAAC,UAAU,CAAU,OAAO,CAAC;QACtC,aAAa,EAAE,EAAE,CAAC,UAAU,CAAkB,eAAe,CAAC;QAC9D,WAAW,EAAE,EAAE,CAAC,UAAU,CAAgB,aAAa,CAAC;QACxD,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC;KAC1C,CAAA;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ export type { CloudDb, TokenCache, GitHubConnection, ProjectDoc, TeamDoc, MembershipDoc, InviteDoc, CustomDomainDoc, EmbedTokenDoc, PlanDoc, AuditEventDoc } from './types.js';
2
+ export { createCloudDb } from './db.js';
3
+ export { signGitHubAppJwt } from './jwt.js';
4
+ export { signState, verifyState } from './state.js';
5
+ export type { StatePayload, StateVerifyResult } from './state.js';
6
+ export { handleTokenRelay } from './relay.js';
7
+ export type { RelayOptions } from './relay.js';
8
+ export { handleWebhook } from './webhook.js';
9
+ export type { WebhookOptions } from './webhook.js';
10
+ export { handleCallback } from './callback.js';
11
+ export type { CallbackOptions } from './callback.js';
12
+ export { handleListTeams, handleCreateTeam, handleGetTeam, handleUpdateTeam } from './team.js';
13
+ export { handleListProjects, handleCreateProject, handleGetProject, handleGenerateApiKey } from './project.js';
14
+ export { handleInviteMember, handleAcceptInvite, handleRevokeInvite } from './invite.js';
15
+ export { handleGetPlans, handleUpgradePlan, handleWebhookSettlement, handleGetUsage, checkLimit, checkFeature } from './billing.js';
16
+ export type { PayClient } from './billing.js';
17
+ export { createCloudCmsHandler, bustEngineCache } from './runtime.js';
18
+ export type { CloudCmsOptions, CloudCmsHandler } from './runtime.js';
19
+ export { signCliToken, verifyCliToken } from './cli.js';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC9K,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAC7C,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE9F,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAE9G,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAExF,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACnI,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACrE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAEpE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export { createCloudDb } from './db.js';
2
+ export { signGitHubAppJwt } from './jwt.js';
3
+ export { signState, verifyState } from './state.js';
4
+ export { handleTokenRelay } from './relay.js';
5
+ export { handleWebhook } from './webhook.js';
6
+ export { handleCallback } from './callback.js';
7
+ // Team management
8
+ export { handleListTeams, handleCreateTeam, handleGetTeam, handleUpdateTeam } from './team.js';
9
+ // Project management
10
+ export { handleListProjects, handleCreateProject, handleGetProject, handleGenerateApiKey } from './project.js';
11
+ // Invite management
12
+ export { handleInviteMember, handleAcceptInvite, handleRevokeInvite } from './invite.js';
13
+ // Billing
14
+ export { handleGetPlans, handleUpgradePlan, handleWebhookSettlement, handleGetUsage, checkLimit, checkFeature } from './billing.js';
15
+ // Cloud CMS runtime
16
+ export { createCloudCmsHandler, bustEngineCache } from './runtime.js';
17
+ // CLI tokens
18
+ export { signCliToken, verifyCliToken } from './cli.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,kBAAkB;AAClB,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC9F,qBAAqB;AACrB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAC9G,oBAAoB;AACpB,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACxF,UAAU;AACV,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEnI,oBAAoB;AACpB,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAErE,aAAa;AACb,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA"}
@@ -0,0 +1,28 @@
1
+ import type { CloudAuthAdapter } from '@airdraft/cloud-auth';
2
+ import type { CloudDb } from './types.js';
3
+ /**
4
+ * POST /v1/teams/:teamSlug/invites
5
+ *
6
+ * Body: `{ email: string; role: 'admin' | 'editor' | 'viewer' }`
7
+ *
8
+ * Creates a pending invite. Requires `owner` or `admin` role.
9
+ * Caller is responsible for sending the invite email (email service wired at app level).
10
+ */
11
+ export declare function handleInviteMember(req: Request, db: CloudDb, auth: CloudAuthAdapter, teamSlug: string): Promise<Response>;
12
+ /**
13
+ * POST /v1/invites/:token/accept
14
+ *
15
+ * Accepts an invite. If the user doesn't exist, the cloud app layer
16
+ * should call `auth.createUser()` before calling this handler.
17
+ *
18
+ * Creates a `MembershipDoc` and deletes the invite.
19
+ * The request must include the authenticated session of the accepting user.
20
+ */
21
+ export declare function handleAcceptInvite(req: Request, db: CloudDb, auth: CloudAuthAdapter, token: string): Promise<Response>;
22
+ /**
23
+ * DELETE /v1/teams/:teamSlug/invites/:token
24
+ *
25
+ * Deletes a pending invite. Requires `owner` or `admin` role.
26
+ */
27
+ export declare function handleRevokeInvite(req: Request, db: CloudDb, auth: CloudAuthAdapter, teamSlug: string, token: string): Promise<Response>;
28
+ //# sourceMappingURL=invite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.d.ts","sourceRoot":"","sources":["../src/invite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AA2BzC;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CAmEnB;AAMD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,QAAQ,CAAC,CA0BnB;AAMD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,QAAQ,CAAC,CAoBnB"}
package/dist/invite.js ADDED
@@ -0,0 +1,155 @@
1
+ import { randomBytes } from 'crypto';
2
+ // ---------------------------------------------------------------------------
3
+ // Helpers
4
+ // ---------------------------------------------------------------------------
5
+ function json(body, status = 200) {
6
+ return new Response(JSON.stringify(body), {
7
+ status,
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+ function err(code, message, status) {
12
+ return json({ error: { code, message } }, status);
13
+ }
14
+ const INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
15
+ function generateInviteToken() {
16
+ return randomBytes(24).toString('hex');
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // handleInviteMember
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * POST /v1/teams/:teamSlug/invites
23
+ *
24
+ * Body: `{ email: string; role: 'admin' | 'editor' | 'viewer' }`
25
+ *
26
+ * Creates a pending invite. Requires `owner` or `admin` role.
27
+ * Caller is responsible for sending the invite email (email service wired at app level).
28
+ */
29
+ export async function handleInviteMember(req, db, auth, teamSlug) {
30
+ const identity = await auth.verifySession(req);
31
+ if (!identity)
32
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
33
+ const team = await db.teams.findOne({ slug: teamSlug });
34
+ if (!team)
35
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
36
+ const membership = await db.memberships.findOne({
37
+ teamId: team._id.toString(),
38
+ userId: identity.userId,
39
+ });
40
+ if (!membership || !['owner', 'admin'].includes(membership.role)) {
41
+ return err('FORBIDDEN', 'Requires owner or admin role', 403);
42
+ }
43
+ let body;
44
+ try {
45
+ body = await req.json();
46
+ }
47
+ catch {
48
+ return err('INVALID_BODY', 'Request body must be JSON', 400);
49
+ }
50
+ const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : '';
51
+ if (!email || !email.includes('@'))
52
+ return err('INVALID_EMAIL', 'Valid email is required', 400);
53
+ const allowedRoles = ['admin', 'editor', 'viewer'];
54
+ const role = allowedRoles.includes(body.role)
55
+ ? body.role
56
+ : null;
57
+ if (!role)
58
+ return err('INVALID_ROLE', 'Role must be admin, editor, or viewer', 400);
59
+ // Check if already a member
60
+ const existingMember = await db.memberships.findOne({
61
+ teamId: team._id.toString(),
62
+ // We can't query by email directly — that's in the auth system. Skip for v1.
63
+ });
64
+ void existingMember; // suppress lint — full check requires auth user lookup, deferred to v1.x
65
+ // Check for existing pending invite
66
+ const existingInvite = await db.invites.findOne({
67
+ teamId: team._id.toString(),
68
+ email,
69
+ expiresAt: { $gt: new Date() },
70
+ });
71
+ if (existingInvite) {
72
+ return err('INVITE_EXISTS', 'A pending invite already exists for this email', 409);
73
+ }
74
+ const { ObjectId } = await import('mongodb');
75
+ const now = new Date();
76
+ const inviteId = new ObjectId();
77
+ const token = generateInviteToken();
78
+ await db.invites.insertOne({
79
+ _id: inviteId,
80
+ teamId: team._id.toString(),
81
+ email,
82
+ role,
83
+ invitedBy: identity.userId,
84
+ token,
85
+ expiresAt: new Date(now.getTime() + INVITE_TTL_MS),
86
+ createdAt: now,
87
+ });
88
+ const invite = await db.invites.findOne({ _id: inviteId });
89
+ return json({ data: invite }, 201);
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // handleAcceptInvite
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * POST /v1/invites/:token/accept
96
+ *
97
+ * Accepts an invite. If the user doesn't exist, the cloud app layer
98
+ * should call `auth.createUser()` before calling this handler.
99
+ *
100
+ * Creates a `MembershipDoc` and deletes the invite.
101
+ * The request must include the authenticated session of the accepting user.
102
+ */
103
+ export async function handleAcceptInvite(req, db, auth, token) {
104
+ const identity = await auth.verifySession(req);
105
+ if (!identity)
106
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
107
+ const invite = await db.invites.findOne({ token });
108
+ if (!invite)
109
+ return err('INVITE_NOT_FOUND', 'Invite not found or already used', 404);
110
+ if (invite.expiresAt < new Date())
111
+ return err('INVITE_EXPIRED', 'Invite has expired', 410);
112
+ // Verify the accepting user's email matches the invite
113
+ if (identity.email.toLowerCase() !== invite.email) {
114
+ return err('EMAIL_MISMATCH', 'Authenticated email does not match invite', 403);
115
+ }
116
+ const { ObjectId } = await import('mongodb');
117
+ await db.memberships.insertOne({
118
+ _id: new ObjectId(),
119
+ teamId: invite.teamId,
120
+ userId: identity.userId,
121
+ role: invite.role,
122
+ createdAt: new Date(),
123
+ });
124
+ await db.invites.deleteOne({ _id: invite._id });
125
+ return json({ data: { teamId: invite.teamId, role: invite.role } });
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // handleRevokeInvite
129
+ // ---------------------------------------------------------------------------
130
+ /**
131
+ * DELETE /v1/teams/:teamSlug/invites/:token
132
+ *
133
+ * Deletes a pending invite. Requires `owner` or `admin` role.
134
+ */
135
+ export async function handleRevokeInvite(req, db, auth, teamSlug, token) {
136
+ const identity = await auth.verifySession(req);
137
+ if (!identity)
138
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
139
+ const team = await db.teams.findOne({ slug: teamSlug });
140
+ if (!team)
141
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
142
+ const membership = await db.memberships.findOne({
143
+ teamId: team._id.toString(),
144
+ userId: identity.userId,
145
+ });
146
+ if (!membership || !['owner', 'admin'].includes(membership.role)) {
147
+ return err('FORBIDDEN', 'Requires owner or admin role', 403);
148
+ }
149
+ const invite = await db.invites.findOne({ token, teamId: team._id.toString() });
150
+ if (!invite)
151
+ return err('INVITE_NOT_FOUND', 'Invite not found', 404);
152
+ await db.invites.deleteOne({ _id: invite._id });
153
+ return new Response(null, { status: 204 });
154
+ }
155
+ //# sourceMappingURL=invite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.js","sourceRoot":"","sources":["../src/invite.ts"],"names":[],"mappings":"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"}
package/dist/jwt.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Signs a GitHub App JWT (RS256, 10-minute lifetime) using the app's private key.
3
+ *
4
+ * @param appId - GitHub App numeric ID (from app settings).
5
+ * @param privateKeyPem - PEM-encoded RSA private key (PKCS#1 or PKCS#8).
6
+ * @returns Signed JWT string for use as `Authorization: Bearer <jwt>`.
7
+ */
8
+ export declare function signGitHubAppJwt(appId: string, privateKeyPem: string): Promise<string>;
9
+ //# sourceMappingURL=jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB5F"}
package/dist/jwt.js ADDED
@@ -0,0 +1,32 @@
1
+ // GitHub App JWT signing using node:crypto (RS256).
2
+ // Produces a 10-minute JWT for authenticating as the GitHub App.
3
+ // Uses node:crypto createSign which accepts both PKCS#1 and PKCS#8 PEM keys.
4
+ import { createSign } from 'node:crypto';
5
+ function base64url(input) {
6
+ const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
7
+ return buf.toString('base64')
8
+ .replace(/\+/g, '-')
9
+ .replace(/\//g, '_')
10
+ .replace(/=+$/, '');
11
+ }
12
+ /**
13
+ * Signs a GitHub App JWT (RS256, 10-minute lifetime) using the app's private key.
14
+ *
15
+ * @param appId - GitHub App numeric ID (from app settings).
16
+ * @param privateKeyPem - PEM-encoded RSA private key (PKCS#1 or PKCS#8).
17
+ * @returns Signed JWT string for use as `Authorization: Bearer <jwt>`.
18
+ */
19
+ export async function signGitHubAppJwt(appId, privateKeyPem) {
20
+ const now = Math.floor(Date.now() / 1000);
21
+ const header = base64url(Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })));
22
+ const payload = base64url(Buffer.from(JSON.stringify({ iat: now - 60, exp: now + 540, iss: appId })));
23
+ const signingInput = `${header}.${payload}`;
24
+ // Normalize literal \n sequences that survive .env parsing (dotenv quirk with some Next.js versions)
25
+ const normalizedKey = privateKeyPem.replace(/\\n/g, '\n');
26
+ // node:crypto createSign handles both PKCS#1 (RSA PRIVATE KEY) and PKCS#8 (PRIVATE KEY)
27
+ const sign = createSign('RSA-SHA256');
28
+ sign.update(signingInput);
29
+ const signature = sign.sign(normalizedKey);
30
+ return `${signingInput}.${base64url(signature)}`;
31
+ }
32
+ //# sourceMappingURL=jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,iEAAiE;AACjE,6EAA6E;AAC7E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,SAAS,SAAS,CAAC,KAA0B;IAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC/D,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;SAC1B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AACvB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAa,EAAE,aAAqB;IACzE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACzC,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;IACnF,MAAM,OAAO,GAAG,SAAS,CACvB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAC3E,CAAA;IACD,MAAM,YAAY,GAAG,GAAG,MAAM,IAAI,OAAO,EAAE,CAAA;IAE3C,qGAAqG;IACrG,MAAM,aAAa,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAEzD,wFAAwF;IACxF,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IACrC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAE1C,OAAO,GAAG,YAAY,IAAI,SAAS,CAAC,SAAS,CAAC,EAAE,CAAA;AAClD,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { CloudAuthAdapter } from '@airdraft/cloud-auth';
2
+ import type { CloudDb } from './types.js';
3
+ /**
4
+ * GET /v1/teams/:teamSlug/projects
5
+ *
6
+ * Returns all projects belonging to a team. Requires membership.
7
+ */
8
+ export declare function handleListProjects(req: Request, db: CloudDb, auth: CloudAuthAdapter, teamSlug: string): Promise<Response>;
9
+ /**
10
+ * POST /v1/teams/:teamSlug/projects
11
+ *
12
+ * Body: `{ name: string }`
13
+ *
14
+ * Creates a new project. Requires `owner` or `admin` role.
15
+ */
16
+ export declare function handleCreateProject(req: Request, db: CloudDb, auth: CloudAuthAdapter, teamSlug: string): Promise<Response>;
17
+ /**
18
+ * GET /v1/projects/:projectSlug
19
+ *
20
+ * Returns project by slug. Resolves team membership from the project's teamId.
21
+ */
22
+ export declare function handleGetProject(req: Request, db: CloudDb, auth: CloudAuthAdapter, projectSlug: string): Promise<Response>;
23
+ /**
24
+ * POST /v1/projects/:projectSlug/api-key
25
+ *
26
+ * Rotates the project API key. Returns the plaintext key ONCE.
27
+ * Stores only the SHA-256 hash. Requires `owner` or `admin` role.
28
+ */
29
+ export declare function handleGenerateApiKey(req: Request, db: CloudDb, auth: CloudAuthAdapter, projectSlug: string): Promise<Response>;
30
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAmCzC;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAMD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CA2CnB;AAMD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAcnB;AAMD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,gBAAgB,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAuBnB"}
@@ -0,0 +1,145 @@
1
+ import { generateApiKey } from '@airdraft/auth';
2
+ // ---------------------------------------------------------------------------
3
+ // Helpers
4
+ // ---------------------------------------------------------------------------
5
+ function json(body, status = 200) {
6
+ return new Response(JSON.stringify(body), {
7
+ status,
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+ function err(code, message, status) {
12
+ return json({ error: { code, message } }, status);
13
+ }
14
+ async function resolveMembership(db, userId, teamSlug) {
15
+ const team = await db.teams.findOne({ slug: teamSlug });
16
+ if (!team)
17
+ return { team: null, membership: null };
18
+ const membership = await db.memberships.findOne({
19
+ teamId: team._id.toString(),
20
+ userId,
21
+ });
22
+ return { team, membership };
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // handleListProjects
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * GET /v1/teams/:teamSlug/projects
29
+ *
30
+ * Returns all projects belonging to a team. Requires membership.
31
+ */
32
+ export async function handleListProjects(req, db, auth, teamSlug) {
33
+ const identity = await auth.verifySession(req);
34
+ if (!identity)
35
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
36
+ const { team, membership } = await resolveMembership(db, identity.userId, teamSlug);
37
+ if (!team)
38
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
39
+ if (!membership)
40
+ return err('FORBIDDEN', 'Not a member of this team', 403);
41
+ const projects = await db.projects.find({ teamId: team._id.toString() }).toArray();
42
+ return json({ data: projects });
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // handleCreateProject
46
+ // ---------------------------------------------------------------------------
47
+ /**
48
+ * POST /v1/teams/:teamSlug/projects
49
+ *
50
+ * Body: `{ name: string }`
51
+ *
52
+ * Creates a new project. Requires `owner` or `admin` role.
53
+ */
54
+ export async function handleCreateProject(req, db, auth, teamSlug) {
55
+ const identity = await auth.verifySession(req);
56
+ if (!identity)
57
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
58
+ const { team, membership } = await resolveMembership(db, identity.userId, teamSlug);
59
+ if (!team)
60
+ return err('TEAM_NOT_FOUND', 'Team not found', 404);
61
+ if (!membership || !['owner', 'admin'].includes(membership.role)) {
62
+ return err('FORBIDDEN', 'Requires owner or admin role', 403);
63
+ }
64
+ let body;
65
+ try {
66
+ body = await req.json();
67
+ }
68
+ catch {
69
+ return err('INVALID_BODY', 'Request body must be JSON', 400);
70
+ }
71
+ const name = typeof body.name === 'string' ? body.name.trim() : '';
72
+ if (!name)
73
+ return err('INVALID_NAME', 'Project name is required', 400);
74
+ const baseSlug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
75
+ if (!baseSlug)
76
+ return err('INVALID_NAME', 'Project name must contain alphanumeric characters', 400);
77
+ const existing = await db.projects.findOne({ teamId: team._id.toString(), slug: baseSlug });
78
+ const slug = existing ? `${baseSlug}-${Date.now().toString(36)}` : baseSlug;
79
+ const { ObjectId } = await import('mongodb');
80
+ const now = new Date();
81
+ const projectId = new ObjectId();
82
+ await db.projects.insertOne({
83
+ _id: projectId,
84
+ teamId: team._id.toString(),
85
+ name,
86
+ slug,
87
+ apiKeyHash: null,
88
+ github: null,
89
+ createdAt: now,
90
+ updatedAt: now,
91
+ });
92
+ const project = await db.projects.findOne({ _id: projectId });
93
+ return json({ data: project }, 201);
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // handleGetProject
97
+ // ---------------------------------------------------------------------------
98
+ /**
99
+ * GET /v1/projects/:projectSlug
100
+ *
101
+ * Returns project by slug. Resolves team membership from the project's teamId.
102
+ */
103
+ export async function handleGetProject(req, db, auth, projectSlug) {
104
+ const identity = await auth.verifySession(req);
105
+ if (!identity)
106
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
107
+ const project = await db.projects.findOne({ slug: projectSlug });
108
+ if (!project)
109
+ return err('PROJECT_NOT_FOUND', 'Project not found', 404);
110
+ const membership = await db.memberships.findOne({
111
+ teamId: project.teamId,
112
+ userId: identity.userId,
113
+ });
114
+ if (!membership)
115
+ return err('FORBIDDEN', 'Not a member of this project team', 403);
116
+ return json({ data: project });
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // handleGenerateApiKey
120
+ // ---------------------------------------------------------------------------
121
+ /**
122
+ * POST /v1/projects/:projectSlug/api-key
123
+ *
124
+ * Rotates the project API key. Returns the plaintext key ONCE.
125
+ * Stores only the SHA-256 hash. Requires `owner` or `admin` role.
126
+ */
127
+ export async function handleGenerateApiKey(req, db, auth, projectSlug) {
128
+ const identity = await auth.verifySession(req);
129
+ if (!identity)
130
+ return err('UNAUTHORIZED', 'Not authenticated', 401);
131
+ const project = await db.projects.findOne({ slug: projectSlug });
132
+ if (!project)
133
+ return err('PROJECT_NOT_FOUND', 'Project not found', 404);
134
+ const membership = await db.memberships.findOne({
135
+ teamId: project.teamId,
136
+ userId: identity.userId,
137
+ });
138
+ if (!membership || !['owner', 'admin'].includes(membership.role)) {
139
+ return err('FORBIDDEN', 'Requires owner or admin role', 403);
140
+ }
141
+ const { key, hash } = generateApiKey();
142
+ await db.projects.updateOne({ _id: project._id }, { $set: { apiKeyHash: hash, updatedAt: new Date() } });
143
+ return json({ data: { key } });
144
+ }
145
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { CloudDb, TokenCache } from './types.js';
2
+ export interface RelayOptions {
3
+ /** GitHub App numeric ID — from `GITHUB_APP_ID` env var on the cloud server. */
4
+ githubAppId: string;
5
+ /** GitHub App PEM private key — from `GITHUB_APP_PRIVATE_KEY` env var. */
6
+ githubAppPrivateKey: string;
7
+ /** Optional L2 token cache (Upstash Redis in production). */
8
+ cache?: TokenCache;
9
+ }
10
+ /**
11
+ * Handler for `POST /v1/github/token`.
12
+ *
13
+ * Exchanges a project API key (`ntk_*`) for a GitHub installation access token.
14
+ * Used by mode 2 self-hosted runtimes that rely on Airdraft's shared GitHub App.
15
+ *
16
+ * Flow:
17
+ * 1. Extract and validate `ntk_*` key from `Authorization: Bearer` header
18
+ * 2. Hash → look up in `projects` collection
19
+ * 3. Apply per-installation rate limit (1 req/s, Redis counter)
20
+ * 4. Return cached token from Redis if still fresh
21
+ * 5. Mint fresh token via GitHub App JWT → cache → return
22
+ *
23
+ * ```ts
24
+ * // In a Next.js route handler:
25
+ * export async function POST(req: Request) {
26
+ * return handleTokenRelay(req, db, {
27
+ * githubAppId: process.env.GITHUB_APP_ID!,
28
+ * githubAppPrivateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
29
+ * cache: redisCache,
30
+ * })
31
+ * }
32
+ * ```
33
+ */
34
+ export declare function handleTokenRelay(req: Request, db: CloudDb, opts: RelayOptions): Promise<Response>;
35
+ //# sourceMappingURL=relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAMrD,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,mBAAmB,EAAE,MAAM,CAAA;IAC3B,6DAA6D;IAC7D,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB;AAsBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,EAAE,EAAE,OAAO,EACX,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,QAAQ,CAAC,CAoEnB"}