@sudobility/shapeshyft_service 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +35 -0
- package/README.md +17 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +55 -0
- package/dist/context.js.map +1 -0
- package/dist/contracts.d.ts +110 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +7 -0
- package/dist/contracts.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-key.d.ts +22 -0
- package/dist/lib/api-key.d.ts.map +1 -0
- package/dist/lib/api-key.js +87 -0
- package/dist/lib/api-key.js.map +1 -0
- package/dist/lib/encryption.d.ts +22 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +41 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/entity-api-key.d.ts +21 -0
- package/dist/lib/entity-api-key.d.ts.map +1 -0
- package/dist/lib/entity-api-key.js +54 -0
- package/dist/lib/entity-api-key.js.map +1 -0
- package/dist/lib/entity-helpers.d.ts +90 -0
- package/dist/lib/entity-helpers.d.ts.map +1 -0
- package/dist/lib/entity-helpers.js +136 -0
- package/dist/lib/entity-helpers.js.map +1 -0
- package/dist/lib/money.d.ts +7 -0
- package/dist/lib/money.d.ts.map +1 -0
- package/dist/lib/money.js +9 -0
- package/dist/lib/money.js.map +1 -0
- package/dist/lib/public-project.d.ts +14 -0
- package/dist/lib/public-project.d.ts.map +1 -0
- package/dist/lib/public-project.js +17 -0
- package/dist/lib/public-project.js.map +1 -0
- package/dist/lib/user-api-key-cache.d.ts +35 -0
- package/dist/lib/user-api-key-cache.d.ts.map +1 -0
- package/dist/lib/user-api-key-cache.js +99 -0
- package/dist/lib/user-api-key-cache.js.map +1 -0
- package/dist/lib/user-api-key.d.ts +46 -0
- package/dist/lib/user-api-key.d.ts.map +1 -0
- package/dist/lib/user-api-key.js +102 -0
- package/dist/lib/user-api-key.js.map +1 -0
- package/dist/middleware/firebaseAuth.d.ts +34 -0
- package/dist/middleware/firebaseAuth.d.ts.map +1 -0
- package/dist/middleware/firebaseAuth.js +133 -0
- package/dist/middleware/firebaseAuth.js.map +1 -0
- package/dist/middleware/rateLimit.d.ts +29 -0
- package/dist/middleware/rateLimit.d.ts.map +1 -0
- package/dist/middleware/rateLimit.js +85 -0
- package/dist/middleware/rateLimit.js.map +1 -0
- package/dist/middleware/subscription.d.ts +8 -0
- package/dist/middleware/subscription.d.ts.map +1 -0
- package/dist/middleware/subscription.js +19 -0
- package/dist/middleware/subscription.js.map +1 -0
- package/dist/routes/ai.d.ts +10 -0
- package/dist/routes/ai.d.ts.map +1 -0
- package/dist/routes/ai.js +698 -0
- package/dist/routes/ai.js.map +1 -0
- package/dist/routes/analytics.d.ts +9 -0
- package/dist/routes/analytics.d.ts.map +1 -0
- package/dist/routes/analytics.js +192 -0
- package/dist/routes/analytics.js.map +1 -0
- package/dist/routes/endpoints.d.ts +10 -0
- package/dist/routes/endpoints.d.ts.map +1 -0
- package/dist/routes/endpoints.js +255 -0
- package/dist/routes/endpoints.js.map +1 -0
- package/dist/routes/entities.d.ts +15 -0
- package/dist/routes/entities.d.ts.map +1 -0
- package/dist/routes/entities.js +332 -0
- package/dist/routes/entities.js.map +1 -0
- package/dist/routes/entity-api-keys.d.ts +17 -0
- package/dist/routes/entity-api-keys.d.ts.map +1 -0
- package/dist/routes/entity-api-keys.js +142 -0
- package/dist/routes/entity-api-keys.js.map +1 -0
- package/dist/routes/invitations.d.ts +15 -0
- package/dist/routes/invitations.d.ts.map +1 -0
- package/dist/routes/invitations.js +58 -0
- package/dist/routes/invitations.js.map +1 -0
- package/dist/routes/projects.d.ts +9 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +241 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/providers.d.ts +9 -0
- package/dist/routes/providers.d.ts.map +1 -0
- package/dist/routes/providers.js +89 -0
- package/dist/routes/providers.js.map +1 -0
- package/dist/routes/ratelimits.d.ts +9 -0
- package/dist/routes/ratelimits.d.ts.map +1 -0
- package/dist/routes/ratelimits.js +171 -0
- package/dist/routes/ratelimits.js.map +1 -0
- package/dist/routes/settings.d.ts +9 -0
- package/dist/routes/settings.d.ts.map +1 -0
- package/dist/routes/settings.js +138 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/storage.d.ts +9 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +180 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/user-api-keys.d.ts +14 -0
- package/dist/routes/user-api-keys.d.ts.map +1 -0
- package/dist/routes/user-api-keys.js +237 -0
- package/dist/routes/user-api-keys.js.map +1 -0
- package/dist/routes/users.d.ts +7 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +102 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/schema/init.d.ts +12 -0
- package/dist/schema/init.d.ts.map +1 -0
- package/dist/schema/init.js +404 -0
- package/dist/schema/init.js.map +1 -0
- package/dist/schema/tables.d.ts +2110 -0
- package/dist/schema/tables.d.ts.map +1 -0
- package/dist/schema/tables.js +251 -0
- package/dist/schema/tables.js.map +1 -0
- package/dist/schemas/index.d.ts +671 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +299 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +70 -0
- package/dist/service.js.map +1 -0
- package/package.json +85 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# shapeshyft_service
|
|
2
|
+
|
|
3
|
+
> **Git policy — never auto-commit or auto-push.** Run `git commit`, `git push`,
|
|
4
|
+
> or publish only when the user explicitly asks in that turn.
|
|
5
|
+
|
|
6
|
+
Shared backend for `shapeshyft_api` and `shaperouter_api`: Drizzle table
|
|
7
|
+
factories, idempotent DDL, Hono routers, auth and rate-limit middleware.
|
|
8
|
+
Stateless LLM work (adapters, prompts, media, provider catalog) lives in
|
|
9
|
+
`@sudobility/shapeshyft_engine`.
|
|
10
|
+
|
|
11
|
+
## The seam
|
|
12
|
+
|
|
13
|
+
The service never knows where a provider API key lives. The app passes a
|
|
14
|
+
`ProviderCredentialResolver`:
|
|
15
|
+
|
|
16
|
+
- `bindEndpoint` — endpoint create/update; returns the `provider` and
|
|
17
|
+
`llmKeyId` to persist, or a `{ status, message }` failure.
|
|
18
|
+
- `resolve` — invoke and prompt-preview time; returns `provider`, `apiKey`,
|
|
19
|
+
`endpointUrl`, `timeoutMs`, or a failure.
|
|
20
|
+
|
|
21
|
+
Optional `InvokeHooks`: `beforeInvoke` (after rate limiting; return a Response to
|
|
22
|
+
stop) and `afterInvoke` (inside the usage_analytics transaction).
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- No `process.env`. Add a field to `ShapeshyftServiceConfig` instead.
|
|
27
|
+
- No reference to `llm_api_keys`. The binding column is `llm_key_id`, nullable,
|
|
28
|
+
no FK here; apps add their own FK.
|
|
29
|
+
- Relative imports carry `.js`.
|
|
30
|
+
- Handler error messages and status codes are part of two products' public APIs.
|
|
31
|
+
|
|
32
|
+
## Tests
|
|
33
|
+
|
|
34
|
+
bun run test # unit, no DB
|
|
35
|
+
TEST_DATABASE_URL=postgresql://localhost:5432/shapeshyft_test bun run test:db
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @sudobility/shapeshyft_service
|
|
2
|
+
|
|
3
|
+
Shared tables, routes and middleware for ShapeShyft-style structured-output APIs.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const service = createShapeshyftService({
|
|
7
|
+
db,
|
|
8
|
+
tables,
|
|
9
|
+
keyPrefixes: { user: "shyft_", entity: "shyftent" },
|
|
10
|
+
encryption,
|
|
11
|
+
auth,
|
|
12
|
+
email,
|
|
13
|
+
credentials, // ProviderCredentialResolver: where provider API keys come from
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
app.route("/api/v1", service.buildRoutes({ mountAdmin }));
|
|
17
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service configuration and the context every router closes over
|
|
3
|
+
*/
|
|
4
|
+
import type { RateLimitsConfig } from "@sudobility/ratelimit_service";
|
|
5
|
+
import { createLLMProvider } from "@sudobility/shapeshyft_engine";
|
|
6
|
+
import type { AuthAdapter, EmailSender, InvokeHooks, Logger, ProviderCredentialResolver, ServiceDb } from "./contracts.js";
|
|
7
|
+
import type { Encryption } from "./lib/encryption.js";
|
|
8
|
+
import { type ProjectApiKeys } from "./lib/api-key.js";
|
|
9
|
+
import { type UserApiKeys } from "./lib/user-api-key.js";
|
|
10
|
+
import { type EntityApiKeyFormat } from "./lib/entity-api-key.js";
|
|
11
|
+
import { type UserApiKeyCache } from "./lib/user-api-key-cache.js";
|
|
12
|
+
import { type EntityAccess } from "./lib/entity-helpers.js";
|
|
13
|
+
import { type SubscriptionAccess } from "./middleware/subscription.js";
|
|
14
|
+
import { type RateLimiting } from "./middleware/rateLimit.js";
|
|
15
|
+
import { type EndpointBindingShapes, type EndpointSchemas } from "./schemas/index.js";
|
|
16
|
+
import type { ServiceTables } from "./schema/tables.js";
|
|
17
|
+
export interface ShapeshyftServiceConfig {
|
|
18
|
+
db: ServiceDb;
|
|
19
|
+
tables: ServiceTables;
|
|
20
|
+
/** e.g. `{ user: "shyft_", entity: "shyftent" }` -- entity has no trailing underscore */
|
|
21
|
+
keyPrefixes: {
|
|
22
|
+
user: string;
|
|
23
|
+
entity: string;
|
|
24
|
+
};
|
|
25
|
+
encryption: Encryption;
|
|
26
|
+
auth: AuthAdapter;
|
|
27
|
+
email: EmailSender;
|
|
28
|
+
credentials: ProviderCredentialResolver;
|
|
29
|
+
/** Extra zod fields validated on endpoint create/update. */
|
|
30
|
+
endpointBinding?: EndpointBindingShapes;
|
|
31
|
+
hooks?: InvokeHooks;
|
|
32
|
+
/** Absent: subscription lookups and invoke rate limiting are skipped. */
|
|
33
|
+
revenueCatApiKey?: string;
|
|
34
|
+
rateLimits?: RateLimitsConfig;
|
|
35
|
+
entitlementDisplayNames?: Record<string, string>;
|
|
36
|
+
/** Default: `console`. */
|
|
37
|
+
logger?: Logger;
|
|
38
|
+
/** Default: the engine's `createLLMProvider`. Tests inject a fake. */
|
|
39
|
+
createProvider?: typeof createLLMProvider;
|
|
40
|
+
}
|
|
41
|
+
export interface ServiceContext {
|
|
42
|
+
db: ServiceDb;
|
|
43
|
+
tables: ServiceTables;
|
|
44
|
+
auth: AuthAdapter;
|
|
45
|
+
email: EmailSender;
|
|
46
|
+
credentials: ProviderCredentialResolver;
|
|
47
|
+
hooks: InvokeHooks;
|
|
48
|
+
logger: Logger;
|
|
49
|
+
createProvider: typeof createLLMProvider;
|
|
50
|
+
revenueCatApiKey: string | undefined;
|
|
51
|
+
encryption: Encryption;
|
|
52
|
+
projectApiKeys: ProjectApiKeys;
|
|
53
|
+
userApiKeys: UserApiKeys;
|
|
54
|
+
userApiKeyCache: UserApiKeyCache;
|
|
55
|
+
entityApiKeyFormat: EntityApiKeyFormat;
|
|
56
|
+
entityAccess: EntityAccess;
|
|
57
|
+
subscription: SubscriptionAccess;
|
|
58
|
+
rateLimiting: RateLimiting;
|
|
59
|
+
schemas: EndpointSchemas;
|
|
60
|
+
}
|
|
61
|
+
export declare function buildContext(config: ShapeshyftServiceConfig): ServiceContext;
|
|
62
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,MAAM,EACN,0BAA0B,EAC1B,SAAS,EACV,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,aAAa,CAAC;IACtB,yFAAyF;IACzF,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,UAAU,EAAE,UAAU,CAAC;IACvB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,WAAW,EAAE,0BAA0B,CAAC;IACxC,4DAA4D;IAC5D,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,cAAc,CAAC,EAAE,OAAO,iBAAiB,CAAC;CAC3C;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,WAAW,EAAE,0BAA0B,CAAC;IACxC,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,iBAAiB,CAAC;IACzC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,cAAc,CAAC;IAC/B,WAAW,EAAE,WAAW,CAAC;IACzB,eAAe,EAAE,eAAe,CAAC;IACjC,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,kBAAkB,CAAC;IACjC,YAAY,EAAE,YAAY,CAAC;IAC3B,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,uBAAuB,GAAG,cAAc,CA2C5E"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service configuration and the context every router closes over
|
|
3
|
+
*/
|
|
4
|
+
import { createLLMProvider } from "@sudobility/shapeshyft_engine";
|
|
5
|
+
import { createProjectApiKeys } from "./lib/api-key.js";
|
|
6
|
+
import { createUserApiKeys } from "./lib/user-api-key.js";
|
|
7
|
+
import { createEntityApiKeyFormat, } from "./lib/entity-api-key.js";
|
|
8
|
+
import { createUserApiKeyCache, } from "./lib/user-api-key-cache.js";
|
|
9
|
+
import { createEntityAccess } from "./lib/entity-helpers.js";
|
|
10
|
+
import { createSubscriptionAccess, } from "./middleware/subscription.js";
|
|
11
|
+
import { createRateLimiting, DEFAULT_ENTITLEMENT_DISPLAY_NAMES, DEFAULT_RATE_LIMITS_CONFIG, } from "./middleware/rateLimit.js";
|
|
12
|
+
import { createEndpointSchemas, } from "./schemas/index.js";
|
|
13
|
+
export function buildContext(config) {
|
|
14
|
+
const logger = config.logger ?? console;
|
|
15
|
+
const userApiKeys = createUserApiKeys({
|
|
16
|
+
prefix: config.keyPrefixes.user,
|
|
17
|
+
encryption: config.encryption,
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
db: config.db,
|
|
21
|
+
tables: config.tables,
|
|
22
|
+
auth: config.auth,
|
|
23
|
+
email: config.email,
|
|
24
|
+
credentials: config.credentials,
|
|
25
|
+
hooks: config.hooks ?? {},
|
|
26
|
+
logger,
|
|
27
|
+
createProvider: config.createProvider ?? createLLMProvider,
|
|
28
|
+
revenueCatApiKey: config.revenueCatApiKey,
|
|
29
|
+
encryption: config.encryption,
|
|
30
|
+
projectApiKeys: createProjectApiKeys(config.encryption),
|
|
31
|
+
userApiKeys,
|
|
32
|
+
userApiKeyCache: createUserApiKeyCache({
|
|
33
|
+
db: config.db,
|
|
34
|
+
tables: config.tables,
|
|
35
|
+
hashUserApiKey: userApiKeys.hashUserApiKey,
|
|
36
|
+
logger,
|
|
37
|
+
}),
|
|
38
|
+
entityApiKeyFormat: createEntityApiKeyFormat(config.keyPrefixes.entity),
|
|
39
|
+
entityAccess: createEntityAccess({
|
|
40
|
+
db: config.db,
|
|
41
|
+
tables: config.tables,
|
|
42
|
+
entityKeyPrefix: config.keyPrefixes.entity,
|
|
43
|
+
}),
|
|
44
|
+
subscription: createSubscriptionAccess(config.revenueCatApiKey),
|
|
45
|
+
rateLimiting: createRateLimiting({
|
|
46
|
+
db: config.db,
|
|
47
|
+
rateLimitCounters: config.tables.rateLimitCounters,
|
|
48
|
+
revenueCatApiKey: config.revenueCatApiKey,
|
|
49
|
+
rateLimitsConfig: config.rateLimits ?? DEFAULT_RATE_LIMITS_CONFIG,
|
|
50
|
+
entitlementDisplayNames: config.entitlementDisplayNames ?? DEFAULT_ENTITLEMENT_DISPLAY_NAMES,
|
|
51
|
+
}),
|
|
52
|
+
schemas: createEndpointSchemas(config.endpointBinding),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAUlE,OAAO,EAAE,oBAAoB,EAAuB,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAoB,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EACL,wBAAwB,GAEzB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,qBAAqB,GAEtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAqB,MAAM,yBAAyB,CAAC;AAChF,OAAO,EACL,wBAAwB,GAEzB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,kBAAkB,EAClB,iCAAiC,EACjC,0BAA0B,GAE3B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,qBAAqB,GAGtB,MAAM,oBAAoB,CAAC;AA8C5B,MAAM,UAAU,YAAY,CAAC,MAA+B;IAC1D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC;IACxC,MAAM,WAAW,GAAG,iBAAiB,CAAC;QACpC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI;QAC/B,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE;QACzB,MAAM;QACN,cAAc,EAAE,MAAM,CAAC,cAAc,IAAI,iBAAiB;QAC1D,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;QACzC,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,cAAc,EAAE,oBAAoB,CAAC,MAAM,CAAC,UAAU,CAAC;QACvD,WAAW;QACX,eAAe,EAAE,qBAAqB,CAAC;YACrC,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,cAAc,EAAE,WAAW,CAAC,cAAc;YAC1C,MAAM;SACP,CAAC;QACF,kBAAkB,EAAE,wBAAwB,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC;QACvE,YAAY,EAAE,kBAAkB,CAAC;YAC/B,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,eAAe,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM;SAC3C,CAAC;QACF,YAAY,EAAE,wBAAwB,CAAC,MAAM,CAAC,gBAAgB,CAAC;QAC/D,YAAY,EAAE,kBAAkB,CAAC;YAC/B,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,iBAAiB;YAClD,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;YACzC,gBAAgB,EAAE,MAAM,CAAC,UAAU,IAAI,0BAA0B;YACjE,uBAAuB,EACrB,MAAM,CAAC,uBAAuB,IAAI,iCAAiC;SACtE,CAAC;QACF,OAAO,EAAE,qBAAqB,CAAC,MAAM,CAAC,eAAe,CAAC;KACvD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview What an app supplies to shapeshyft_service
|
|
3
|
+
* @description The provider-credential seam, invoke hooks, and the adapters that
|
|
4
|
+
* replace module-level singletons (auth, email, logging).
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from "hono";
|
|
7
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
8
|
+
import type { DecodedIdToken } from "firebase-admin/auth";
|
|
9
|
+
import type { getUserInfo, isAnonymousUser, isSiteAdmin } from "@sudobility/auth_service";
|
|
10
|
+
import type { EndpointBase, LlmProvider } from "@sudobility/shapeshyft_engine/types";
|
|
11
|
+
import type { ServiceTables } from "./schema/tables.js";
|
|
12
|
+
/** The Drizzle database the app connects. Tables are passed separately. */
|
|
13
|
+
export type ServiceDb = PostgresJsDatabase<any>;
|
|
14
|
+
/** The `tx` handed to a `db.transaction` callback. */
|
|
15
|
+
export type DbTransaction = Parameters<Parameters<ServiceDb["transaction"]>[0]>[0];
|
|
16
|
+
export type EndpointRow = ServiceTables["endpoints"]["$inferSelect"];
|
|
17
|
+
export type EntityRow = ServiceTables["entities"]["$inferSelect"];
|
|
18
|
+
export type ProjectRow = ServiceTables["projects"]["$inferSelect"];
|
|
19
|
+
/** An endpoint as the service's routes return it. */
|
|
20
|
+
export interface EndpointRecord extends EndpointBase {
|
|
21
|
+
llm_key_id: string | null;
|
|
22
|
+
provider: LlmProvider | null;
|
|
23
|
+
}
|
|
24
|
+
/** A refusal. `message` is returned to the caller verbatim as `errorResponse(message)`. */
|
|
25
|
+
export interface ResolverFailure {
|
|
26
|
+
ok: false;
|
|
27
|
+
status: 400 | 404 | 500 | 503;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
/** What to persist on an endpoint for its provider binding. */
|
|
31
|
+
export interface EndpointBinding {
|
|
32
|
+
ok: true;
|
|
33
|
+
provider: LlmProvider;
|
|
34
|
+
/** ShapeShyft: the LlmApiKey UUID. Products without per-entity keys: null. */
|
|
35
|
+
llmKeyId: string | null;
|
|
36
|
+
}
|
|
37
|
+
/** A live credential for one call. `provider` is authoritative for the call. */
|
|
38
|
+
export interface ResolvedCredential {
|
|
39
|
+
ok: true;
|
|
40
|
+
provider: LlmProvider;
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
endpointUrl?: string;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ProviderCredentialResolver {
|
|
46
|
+
/**
|
|
47
|
+
* Endpoint create (no `current`) or update (`current` set). `body` is the
|
|
48
|
+
* validated request body, including the app's `endpointBinding` fields.
|
|
49
|
+
*/
|
|
50
|
+
bindEndpoint(ctx: {
|
|
51
|
+
entityId: string;
|
|
52
|
+
body: Record<string, unknown>;
|
|
53
|
+
current?: EndpointRow;
|
|
54
|
+
}): Promise<EndpointBinding | ResolverFailure>;
|
|
55
|
+
/** Invoke and prompt-preview time. */
|
|
56
|
+
resolve(ctx: {
|
|
57
|
+
entityId: string;
|
|
58
|
+
endpoint: EndpointRow;
|
|
59
|
+
}): Promise<ResolvedCredential | ResolverFailure>;
|
|
60
|
+
}
|
|
61
|
+
export interface InvokeHooks {
|
|
62
|
+
/**
|
|
63
|
+
* After rate limiting, before the provider call. Return a Response to stop
|
|
64
|
+
* the request with it (e.g. 402 insufficient credit). Not run for /prompt.
|
|
65
|
+
*/
|
|
66
|
+
beforeInvoke?(ctx: {
|
|
67
|
+
c: Context;
|
|
68
|
+
entity: EntityRow;
|
|
69
|
+
project: ProjectRow;
|
|
70
|
+
endpoint: EndpointRow;
|
|
71
|
+
provider: LlmProvider;
|
|
72
|
+
}): Promise<Response | void>;
|
|
73
|
+
/**
|
|
74
|
+
* After a successful provider call, inside the transaction that inserts the
|
|
75
|
+
* usage_analytics row. Throwing rolls that row back and fails the request 500.
|
|
76
|
+
*/
|
|
77
|
+
afterInvoke?(ctx: {
|
|
78
|
+
tx: DbTransaction;
|
|
79
|
+
entity: EntityRow;
|
|
80
|
+
endpoint: EndpointRow;
|
|
81
|
+
usageAnalyticsId: string;
|
|
82
|
+
provider: LlmProvider;
|
|
83
|
+
model: string;
|
|
84
|
+
usage: {
|
|
85
|
+
promptTokens: number;
|
|
86
|
+
completionTokens: number;
|
|
87
|
+
};
|
|
88
|
+
providerCostMicroCents: bigint;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
/** Auth functions the app initialises (auth_service is process-global). */
|
|
92
|
+
export interface AuthAdapter {
|
|
93
|
+
verifyIdToken(token: string): Promise<DecodedIdToken>;
|
|
94
|
+
isSiteAdmin: typeof isSiteAdmin;
|
|
95
|
+
isAnonymousUser: typeof isAnonymousUser;
|
|
96
|
+
getUserInfo: typeof getUserInfo;
|
|
97
|
+
}
|
|
98
|
+
export interface EmailSender {
|
|
99
|
+
sendInvitationEmail(params: {
|
|
100
|
+
recipientEmail: string;
|
|
101
|
+
entityName: string;
|
|
102
|
+
}): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
/** `console` satisfies this; it is the default. */
|
|
105
|
+
export interface Logger {
|
|
106
|
+
log(...args: unknown[]): void;
|
|
107
|
+
warn(...args: unknown[]): void;
|
|
108
|
+
error(...args: unknown[]): void;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @sudobility/shapeshyft_service
|
|
3
|
+
* @description Tables, routes and middleware shared by ShapeShyft and
|
|
4
|
+
* ShapeRouter. Provider credentials come from the app via
|
|
5
|
+
* ProviderCredentialResolver; billing seams via InvokeHooks.
|
|
6
|
+
*/
|
|
7
|
+
export { createShapeshyftService, type BuildRoutesOptions, type ShapeshyftService, } from "./service.js";
|
|
8
|
+
export { buildContext, type ServiceContext, type ShapeshyftServiceConfig, } from "./context.js";
|
|
9
|
+
export type { AuthAdapter, DbTransaction, EmailSender, EndpointBinding, EndpointRecord, EndpointRow, EntityRow, InvokeHooks, Logger, ProjectRow, ProviderCredentialResolver, ResolvedCredential, ResolverFailure, ServiceDb, } from "./contracts.js";
|
|
10
|
+
export { createServiceTables, LLM_PROVIDER_VALUES, type ServiceTables, } from "./schema/tables.js";
|
|
11
|
+
export { initServiceTables } from "./schema/init.js";
|
|
12
|
+
export { createEncryption, generateEncryptionKey, type Encryption, } from "./lib/encryption.js";
|
|
13
|
+
export { toMicroCents } from "./lib/money.js";
|
|
14
|
+
export { getActor, getPermissionErrorStatus, userActor, ENTITY_API_KEY_PERMISSIONS, type EntityAccess, type EntityActor, type EntityPermissionResult, } from "./lib/entity-helpers.js";
|
|
15
|
+
export { DEFAULT_RATE_LIMITS_CONFIG, DEFAULT_ENTITLEMENT_DISPLAY_NAMES, } from "./middleware/rateLimit.js";
|
|
16
|
+
export * from "./schemas/index.js";
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,WAAW,EACX,SAAS,EACT,WAAW,EACX,MAAM,EACN,UAAU,EACV,0BAA0B,EAC1B,kBAAkB,EAClB,eAAe,EACf,SAAS,GACV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,UAAU,GAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,QAAQ,EACR,wBAAwB,EACxB,SAAS,EACT,0BAA0B,EAC1B,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,sBAAsB,GAC5B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,0BAA0B,EAC1B,iCAAiC,GAClC,MAAM,2BAA2B,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @sudobility/shapeshyft_service
|
|
3
|
+
* @description Tables, routes and middleware shared by ShapeShyft and
|
|
4
|
+
* ShapeRouter. Provider credentials come from the app via
|
|
5
|
+
* ProviderCredentialResolver; billing seams via InvokeHooks.
|
|
6
|
+
*/
|
|
7
|
+
export { createShapeshyftService, } from "./service.js";
|
|
8
|
+
export { buildContext, } from "./context.js";
|
|
9
|
+
export { createServiceTables, LLM_PROVIDER_VALUES, } from "./schema/tables.js";
|
|
10
|
+
export { initServiceTables } from "./schema/init.js";
|
|
11
|
+
export { createEncryption, generateEncryptionKey, } from "./lib/encryption.js";
|
|
12
|
+
export { toMicroCents } from "./lib/money.js";
|
|
13
|
+
export { getActor, getPermissionErrorStatus, userActor, ENTITY_API_KEY_PERMISSIONS, } from "./lib/entity-helpers.js";
|
|
14
|
+
export { DEFAULT_RATE_LIMITS_CONFIG, DEFAULT_ENTITLEMENT_DISPLAY_NAMES, } from "./middleware/rateLimit.js";
|
|
15
|
+
export * from "./schemas/index.js";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,uBAAuB,GAGxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,GAGb,MAAM,cAAc,CAAC;AAiBtB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,GAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,QAAQ,EACR,wBAAwB,EACxB,SAAS,EACT,0BAA0B,GAI3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,0BAA0B,EAC1B,iCAAiC,GAClC,MAAM,2BAA2B,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Project API key management
|
|
3
|
+
* @description Generates, encrypts, decrypts, and validates project API keys.
|
|
4
|
+
* Keys use the format `sk_live_<base64url-random>` and are validated
|
|
5
|
+
* using timing-safe comparison to prevent timing attacks.
|
|
6
|
+
*/
|
|
7
|
+
import type { Encryption } from "./encryption.js";
|
|
8
|
+
export declare function createProjectApiKeys(encryption: Encryption): {
|
|
9
|
+
generateProjectApiKey: () => {
|
|
10
|
+
key: string;
|
|
11
|
+
prefix: string;
|
|
12
|
+
};
|
|
13
|
+
encryptProjectApiKey: (key: string) => {
|
|
14
|
+
encrypted: string;
|
|
15
|
+
iv: string;
|
|
16
|
+
};
|
|
17
|
+
decryptProjectApiKey: (encrypted: string, iv: string) => string;
|
|
18
|
+
validateProjectApiKey: (providedKey: string, encryptedKey: string, iv: string) => boolean;
|
|
19
|
+
isValidApiKeyFormat: (key: string) => boolean;
|
|
20
|
+
};
|
|
21
|
+
export type ProjectApiKeys = ReturnType<typeof createProjectApiKeys>;
|
|
22
|
+
//# sourceMappingURL=api-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.d.ts","sourceRoot":"","sources":["../../src/lib/api-key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAQlD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU;iCAQvB;QAChC,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;KAChB;gCAoBkC,MAAM,KAAG;QAC1C,SAAS,EAAE,MAAM,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;KACZ;sCAUwC,MAAM,MAAM,MAAM,KAAG,MAAM;yCAarD,MAAM,gBACL,MAAM,MAChB,MAAM,KACT,OAAO;+BAuBwB,MAAM,KAAG,OAAO;EAWnD;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Project API key management
|
|
3
|
+
* @description Generates, encrypts, decrypts, and validates project API keys.
|
|
4
|
+
* Keys use the format `sk_live_<base64url-random>` and are validated
|
|
5
|
+
* using timing-safe comparison to prevent timing attacks.
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
8
|
+
// API Key prefix for identification
|
|
9
|
+
const API_KEY_PREFIX = "sk_live_";
|
|
10
|
+
// Random bytes of entropy (32 bytes = 256 bits)
|
|
11
|
+
const KEY_BYTES_LENGTH = 32;
|
|
12
|
+
export function createProjectApiKeys(encryption) {
|
|
13
|
+
const { encryptApiKey, decryptApiKey } = encryption;
|
|
14
|
+
/**
|
|
15
|
+
* Generate a new API key with prefix
|
|
16
|
+
* Format: sk_live_<base64url-encoded-random-bytes>
|
|
17
|
+
* @returns Object with full key and prefix for display
|
|
18
|
+
*/
|
|
19
|
+
function generateProjectApiKey() {
|
|
20
|
+
const randomPart = randomBytes(KEY_BYTES_LENGTH)
|
|
21
|
+
.toString("base64url")
|
|
22
|
+
.replace(/[=]/g, ""); // Remove padding
|
|
23
|
+
const fullKey = `${API_KEY_PREFIX}${randomPart}`;
|
|
24
|
+
// Prefix shows first 12 characters (sk_live_ + first 4 random chars)
|
|
25
|
+
const prefix = fullKey.substring(0, 12);
|
|
26
|
+
return {
|
|
27
|
+
key: fullKey,
|
|
28
|
+
prefix,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Encrypt a project API key for storage
|
|
33
|
+
* @param key The plain text API key
|
|
34
|
+
* @returns Object with encrypted value and IV
|
|
35
|
+
*/
|
|
36
|
+
function encryptProjectApiKey(key) {
|
|
37
|
+
return encryptApiKey(key);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decrypt a stored project API key
|
|
41
|
+
* @param encrypted The encrypted API key (hex string)
|
|
42
|
+
* @param iv The initialization vector (hex string)
|
|
43
|
+
* @returns The decrypted plain text API key
|
|
44
|
+
*/
|
|
45
|
+
function decryptProjectApiKey(encrypted, iv) {
|
|
46
|
+
return decryptApiKey(encrypted, iv);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate a provided API key against an encrypted stored key
|
|
50
|
+
* Uses timing-safe comparison to prevent timing attacks
|
|
51
|
+
* @param providedKey The API key provided by the caller
|
|
52
|
+
* @param encryptedKey The encrypted stored API key
|
|
53
|
+
* @param iv The IV used for encryption
|
|
54
|
+
* @returns true if keys match, false otherwise
|
|
55
|
+
*/
|
|
56
|
+
function validateProjectApiKey(providedKey, encryptedKey, iv) {
|
|
57
|
+
try {
|
|
58
|
+
const storedKey = decryptProjectApiKey(encryptedKey, iv);
|
|
59
|
+
// Use timing-safe comparison
|
|
60
|
+
const providedBuffer = Buffer.from(providedKey, "utf8");
|
|
61
|
+
const storedBuffer = Buffer.from(storedKey, "utf8");
|
|
62
|
+
if (providedBuffer.length !== storedBuffer.length) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return timingSafeEqual(providedBuffer, storedBuffer);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if a string looks like a valid API key format
|
|
73
|
+
* @param key The string to check
|
|
74
|
+
* @returns true if format is valid
|
|
75
|
+
*/
|
|
76
|
+
function isValidApiKeyFormat(key) {
|
|
77
|
+
return key.startsWith(API_KEY_PREFIX) && key.length > API_KEY_PREFIX.length;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
generateProjectApiKey,
|
|
81
|
+
encryptProjectApiKey,
|
|
82
|
+
decryptProjectApiKey,
|
|
83
|
+
validateProjectApiKey,
|
|
84
|
+
isValidApiKeyFormat,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.js","sourceRoot":"","sources":["../../src/lib/api-key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAGtD,oCAAoC;AACpC,MAAM,cAAc,GAAG,UAAU,CAAC;AAElC,gDAAgD;AAChD,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,MAAM,UAAU,oBAAoB,CAAC,UAAsB;IACzD,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,UAAU,CAAC;IAEpD;;;;OAIG;IACH,SAAS,qBAAqB;QAI5B,MAAM,UAAU,GAAG,WAAW,CAAC,gBAAgB,CAAC;aAC7C,QAAQ,CAAC,WAAW,CAAC;aACrB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,iBAAiB;QAEzC,MAAM,OAAO,GAAG,GAAG,cAAc,GAAG,UAAU,EAAE,CAAC;QACjD,qEAAqE;QACrE,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAExC,OAAO;YACL,GAAG,EAAE,OAAO;YACZ,MAAM;SACP,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,SAAS,oBAAoB,CAAC,GAAW;QAIvC,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;OAKG;IACH,SAAS,oBAAoB,CAAC,SAAiB,EAAE,EAAU;QACzD,OAAO,aAAa,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACtC,CAAC;IAED;;;;;;;OAOG;IACH,SAAS,qBAAqB,CAC5B,WAAmB,EACnB,YAAoB,EACpB,EAAU;QAEV,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,oBAAoB,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;YAEzD,6BAA6B;YAC7B,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YACxD,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAEpD,IAAI,cAAc,CAAC,MAAM,KAAK,YAAY,CAAC,MAAM,EAAE,CAAC;gBAClD,OAAO,KAAK,CAAC;YACf,CAAC;YAED,OAAO,eAAe,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,SAAS,mBAAmB,CAAC,GAAW;QACtC,OAAO,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC;IAC9E,CAAC;IAED,OAAO;QACL,qBAAqB;QACrB,oBAAoB;QACpB,oBAAoB;QACpB,qBAAqB;QACrB,mBAAmB;KACpB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AES-256-CBC encryption for credentials at rest
|
|
3
|
+
* @description The key is read through a getter on every call, so an app that
|
|
4
|
+
* starts without ENCRYPTION_KEY fails on first use -- the behaviour
|
|
5
|
+
* shapeshyft_api had when this read the environment directly.
|
|
6
|
+
*/
|
|
7
|
+
export interface Encryption {
|
|
8
|
+
/** Encrypt plain text; returns hex ciphertext and hex IV. */
|
|
9
|
+
encryptApiKey(plainText: string): {
|
|
10
|
+
encrypted: string;
|
|
11
|
+
iv: string;
|
|
12
|
+
};
|
|
13
|
+
/** Decrypt hex ciphertext with its hex IV. */
|
|
14
|
+
decryptApiKey(encrypted: string, ivHex: string): string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @param getKeyHex Returns the 64-hex-character (32-byte) key. Called per operation.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createEncryption(getKeyHex: () => string): Encryption;
|
|
20
|
+
/** A new random key suitable for ENCRYPTION_KEY. */
|
|
21
|
+
export declare function generateEncryptionKey(): string;
|
|
22
|
+
//# sourceMappingURL=encryption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../src/lib/encryption.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,8CAA8C;IAC9C,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACzD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,MAAM,GAAG,UAAU,CA4BpE;AAED,oDAAoD;AACpD,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AES-256-CBC encryption for credentials at rest
|
|
3
|
+
* @description The key is read through a getter on every call, so an app that
|
|
4
|
+
* starts without ENCRYPTION_KEY fails on first use -- the behaviour
|
|
5
|
+
* shapeshyft_api had when this read the environment directly.
|
|
6
|
+
*/
|
|
7
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
8
|
+
const ALGORITHM = "aes-256-cbc";
|
|
9
|
+
const IV_LENGTH = 16; // AES block size
|
|
10
|
+
/**
|
|
11
|
+
* @param getKeyHex Returns the 64-hex-character (32-byte) key. Called per operation.
|
|
12
|
+
*/
|
|
13
|
+
export function createEncryption(getKeyHex) {
|
|
14
|
+
function key() {
|
|
15
|
+
const keyHex = getKeyHex();
|
|
16
|
+
if (keyHex.length !== 64) {
|
|
17
|
+
throw new Error("ENCRYPTION_KEY must be 64 hex characters (32 bytes)");
|
|
18
|
+
}
|
|
19
|
+
return Buffer.from(keyHex, "hex");
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
encryptApiKey(plainText) {
|
|
23
|
+
const iv = randomBytes(IV_LENGTH);
|
|
24
|
+
const cipher = createCipheriv(ALGORITHM, key(), iv);
|
|
25
|
+
let encrypted = cipher.update(plainText, "utf8", "hex");
|
|
26
|
+
encrypted += cipher.final("hex");
|
|
27
|
+
return { encrypted, iv: iv.toString("hex") };
|
|
28
|
+
},
|
|
29
|
+
decryptApiKey(encrypted, ivHex) {
|
|
30
|
+
const decipher = createDecipheriv(ALGORITHM, key(), Buffer.from(ivHex, "hex"));
|
|
31
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
32
|
+
decrypted += decipher.final("utf8");
|
|
33
|
+
return decrypted;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** A new random key suitable for ENCRYPTION_KEY. */
|
|
38
|
+
export function generateEncryptionKey() {
|
|
39
|
+
return randomBytes(32).toString("hex");
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=encryption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encryption.js","sourceRoot":"","sources":["../../src/lib/encryption.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAEvE,MAAM,SAAS,GAAG,aAAa,CAAC;AAChC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC,iBAAiB;AASvC;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAuB;IACtD,SAAS,GAAG;QACV,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,OAAO;QACL,aAAa,CAAC,SAAS;YACrB,MAAM,EAAE,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;YAClC,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;YACpD,IAAI,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;YACxD,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACjC,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,CAAC;QACD,aAAa,CAAC,SAAS,EAAE,KAAK;YAC5B,MAAM,QAAQ,GAAG,gBAAgB,CAC/B,SAAS,EACT,GAAG,EAAE,EACL,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAC1B,CAAC;YACF,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;YAC1D,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACpC,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,qBAAqB;IACnC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Entity API key header conventions
|
|
3
|
+
* @description An entity key (`<prefix>_...`) authenticates a caller as the
|
|
4
|
+
* *entity* rather than as a person: CI jobs, deployment scripts, and MCP clients
|
|
5
|
+
* that must keep working when the member who created them leaves. Storage,
|
|
6
|
+
* generation, and verification live in `@sudobility/entity_service`; this
|
|
7
|
+
* module owns only the product's prefix and the header rules.
|
|
8
|
+
*
|
|
9
|
+
* Three other credentials exist and must not be confused with this one:
|
|
10
|
+
* - personal key (user prefix), authenticates a *user* (see user-api-key.ts)
|
|
11
|
+
* - `sk_live_...` project key, authenticates callers of an AI endpoint
|
|
12
|
+
* - Firebase token browser session credential
|
|
13
|
+
*/
|
|
14
|
+
export declare function createEntityApiKeyFormat(prefix: string): {
|
|
15
|
+
prefix: string;
|
|
16
|
+
prefixWithSeparator: string;
|
|
17
|
+
isEntityApiKeyFormat: (value: string) => boolean;
|
|
18
|
+
extractEntityApiKeyFromHeaders: (getHeader: (name: string) => string | undefined) => string | null;
|
|
19
|
+
};
|
|
20
|
+
export type EntityApiKeyFormat = ReturnType<typeof createEntityApiKeyFormat>;
|
|
21
|
+
//# sourceMappingURL=entity-api-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-api-key.d.ts","sourceRoot":"","sources":["../../src/lib/entity-api-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM;;;kCAOhB,MAAM,KAAG,OAAO;gDAkBxC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,KAC9C,MAAM,GAAG,IAAI;EAqBjB;AAED,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Entity API key header conventions
|
|
3
|
+
* @description An entity key (`<prefix>_...`) authenticates a caller as the
|
|
4
|
+
* *entity* rather than as a person: CI jobs, deployment scripts, and MCP clients
|
|
5
|
+
* that must keep working when the member who created them leaves. Storage,
|
|
6
|
+
* generation, and verification live in `@sudobility/entity_service`; this
|
|
7
|
+
* module owns only the product's prefix and the header rules.
|
|
8
|
+
*
|
|
9
|
+
* Three other credentials exist and must not be confused with this one:
|
|
10
|
+
* - personal key (user prefix), authenticates a *user* (see user-api-key.ts)
|
|
11
|
+
* - `sk_live_...` project key, authenticates callers of an AI endpoint
|
|
12
|
+
* - Firebase token browser session credential
|
|
13
|
+
*/
|
|
14
|
+
export function createEntityApiKeyFormat(prefix) {
|
|
15
|
+
const prefixWithSeparator = `${prefix}_`;
|
|
16
|
+
/**
|
|
17
|
+
* Check whether a string looks like an entity API key for this product.
|
|
18
|
+
* Used to route an incoming credential to entity auth instead of user auth.
|
|
19
|
+
*/
|
|
20
|
+
function isEntityApiKeyFormat(value) {
|
|
21
|
+
return (value.startsWith(prefixWithSeparator) &&
|
|
22
|
+
value.length > prefixWithSeparator.length);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract an entity API key from request headers.
|
|
26
|
+
*
|
|
27
|
+
* Accepts `X-API-Key: <prefix>_...` (preferred, unambiguous) and
|
|
28
|
+
* `Authorization: Bearer <prefix>_...`. Anything without the prefix is left
|
|
29
|
+
* alone so it can be tried as a personal key or a Firebase ID token instead.
|
|
30
|
+
*
|
|
31
|
+
* @param getHeader Reads a request header by name, case-insensitively
|
|
32
|
+
* @returns The key, or null when the request carries none
|
|
33
|
+
*/
|
|
34
|
+
function extractEntityApiKeyFromHeaders(getHeader) {
|
|
35
|
+
const headerKey = getHeader("X-API-Key");
|
|
36
|
+
if (headerKey && isEntityApiKeyFormat(headerKey))
|
|
37
|
+
return headerKey;
|
|
38
|
+
const authHeader = getHeader("Authorization");
|
|
39
|
+
if (authHeader) {
|
|
40
|
+
const [type, token] = authHeader.split(" ");
|
|
41
|
+
if (type === "Bearer" && token && isEntityApiKeyFormat(token)) {
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
prefix,
|
|
49
|
+
prefixWithSeparator,
|
|
50
|
+
isEntityApiKeyFormat,
|
|
51
|
+
extractEntityApiKeyFromHeaders,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=entity-api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-api-key.js","sourceRoot":"","sources":["../../src/lib/entity-api-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,UAAU,wBAAwB,CAAC,MAAc;IACrD,MAAM,mBAAmB,GAAG,GAAG,MAAM,GAAG,CAAC;IAEzC;;;OAGG;IACH,SAAS,oBAAoB,CAAC,KAAa;QACzC,OAAO,CACL,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC;YACrC,KAAK,CAAC,MAAM,GAAG,mBAAmB,CAAC,MAAM,CAC1C,CAAC;IACJ,CAAC;IAED;;;;;;;;;OASG;IACH,SAAS,8BAA8B,CACrC,SAA+C;QAE/C,MAAM,SAAS,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;QACzC,IAAI,SAAS,IAAI,oBAAoB,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;QAEnE,MAAM,UAAU,GAAG,SAAS,CAAC,eAAe,CAAC,CAAC;QAC9C,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5C,IAAI,IAAI,KAAK,QAAQ,IAAI,KAAK,IAAI,oBAAoB,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9D,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,MAAM;QACN,mBAAmB;QACnB,oBAAoB;QACpB,8BAA8B;KAC/B,CAAC;AACJ,CAAC"}
|