@smartcrab/contracts-management 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 (73) hide show
  1. package/LICENSE +202 -0
  2. package/dist/api-keys.d.ts +251 -0
  3. package/dist/api-keys.js +78 -0
  4. package/dist/audit-events.d.ts +94 -0
  5. package/dist/audit-events.js +45 -0
  6. package/dist/billing.d.ts +30 -0
  7. package/dist/billing.js +30 -0
  8. package/dist/clients.d.ts +328 -0
  9. package/dist/clients.js +141 -0
  10. package/dist/common.d.ts +95 -0
  11. package/dist/common.js +99 -0
  12. package/dist/connections.d.ts +201 -0
  13. package/dist/connections.js +107 -0
  14. package/dist/domains.d.ts +167 -0
  15. package/dist/domains.js +74 -0
  16. package/dist/environments.d.ts +194 -0
  17. package/dist/environments.js +101 -0
  18. package/dist/index.d.ts +27 -0
  19. package/dist/index.js +27 -0
  20. package/dist/primitives.d.ts +49 -0
  21. package/dist/primitives.js +56 -0
  22. package/dist/projects.d.ts +78 -0
  23. package/dist/projects.js +49 -0
  24. package/dist/rbac.d.ts +222 -0
  25. package/dist/rbac.js +226 -0
  26. package/dist/usage.d.ts +72 -0
  27. package/dist/usage.js +52 -0
  28. package/dist/user-jobs.d.ts +253 -0
  29. package/dist/user-jobs.js +95 -0
  30. package/dist/user-subresources.d.ts +161 -0
  31. package/dist/user-subresources.js +85 -0
  32. package/dist/users.d.ts +193 -0
  33. package/dist/users.js +112 -0
  34. package/dist/webhooks.d.ts +306 -0
  35. package/dist/webhooks.js +134 -0
  36. package/dist/workspaces.d.ts +172 -0
  37. package/dist/workspaces.js +77 -0
  38. package/package.json +42 -0
  39. package/src/api-keys.test.ts +86 -0
  40. package/src/api-keys.ts +97 -0
  41. package/src/audit-events.test.ts +44 -0
  42. package/src/audit-events.ts +57 -0
  43. package/src/billing.test.ts +57 -0
  44. package/src/billing.ts +46 -0
  45. package/src/clients.test.ts +169 -0
  46. package/src/clients.ts +182 -0
  47. package/src/common.test.ts +102 -0
  48. package/src/common.ts +127 -0
  49. package/src/connections.test.ts +65 -0
  50. package/src/connections.ts +134 -0
  51. package/src/domains.test.ts +48 -0
  52. package/src/domains.ts +104 -0
  53. package/src/environments.test.ts +67 -0
  54. package/src/environments.ts +133 -0
  55. package/src/index.ts +27 -0
  56. package/src/primitives.ts +100 -0
  57. package/src/projects.test.ts +49 -0
  58. package/src/projects.ts +72 -0
  59. package/src/rbac.test.ts +148 -0
  60. package/src/rbac.ts +250 -0
  61. package/src/secret-fields.test.ts +84 -0
  62. package/src/usage.test.ts +57 -0
  63. package/src/usage.ts +66 -0
  64. package/src/user-jobs.test.ts +86 -0
  65. package/src/user-jobs.ts +120 -0
  66. package/src/user-subresources.test.ts +115 -0
  67. package/src/user-subresources.ts +128 -0
  68. package/src/users.test.ts +91 -0
  69. package/src/users.ts +149 -0
  70. package/src/webhooks.test.ts +187 -0
  71. package/src/webhooks.ts +182 -0
  72. package/src/workspaces.test.ts +127 -0
  73. package/src/workspaces.ts +106 -0
@@ -0,0 +1,134 @@
1
+ import { httpsUrlSchema } from "@smartcrab/contracts-public";
2
+ import { z } from "zod";
3
+ import { cursorPageSchema, paginationQuerySchema } from "./common.js";
4
+ import { deliveryIdSchema, environmentIdSchema, eventIdSchema, webhookIdSchema, } from "./primitives.js";
5
+ /**
6
+ * `webhook_endpoints` (design.md §11.13) and the Customer Webhook event
7
+ * catalog (design.md §29.4's "イベント例" list, treated as the canonical
8
+ * enum since no other source enumerates event types). `secret_ciphertext`/
9
+ * `secret_iv` are excluded per the assignment's forbidden-field list; like
10
+ * `management_api_keys` (§11.12) and client secrets, the plaintext webhook
11
+ * signing secret is shown exactly once, on create/rotate.
12
+ */
13
+ export const WEBHOOK_EVENT_TYPES = [
14
+ "user.created",
15
+ "user.updated",
16
+ "user.deleted",
17
+ "user.blocked",
18
+ "identity.linked",
19
+ "identity.unlinked",
20
+ "passkey.created",
21
+ "passkey.deleted",
22
+ "session.created",
23
+ "session.revoked",
24
+ "login.succeeded",
25
+ "login.failed",
26
+ "email.delivery.bounced",
27
+ "mau.threshold_reached",
28
+ ];
29
+ export const webhookEventTypeSchema = z.enum(WEBHOOK_EVENT_TYPES);
30
+ export const WEBHOOK_STATUSES = ["active", "disabled"];
31
+ export const webhookStatusSchema = z.enum(WEBHOOK_STATUSES);
32
+ export const webhookSchema = z.strictObject({
33
+ id: webhookIdSchema,
34
+ environment_id: environmentIdSchema,
35
+ url: httpsUrlSchema,
36
+ event_types: z.array(webhookEventTypeSchema).min(1),
37
+ status: webhookStatusSchema,
38
+ created_at: z.iso.datetime(),
39
+ updated_at: z.iso.datetime(),
40
+ });
41
+ // ---------------------------------------------------------------------------
42
+ // GET /webhooks
43
+ // ---------------------------------------------------------------------------
44
+ export const webhookListQuerySchema = paginationQuerySchema.extend({
45
+ environment_id: environmentIdSchema,
46
+ });
47
+ export const webhookListResponseSchema = cursorPageSchema(webhookSchema);
48
+ // ---------------------------------------------------------------------------
49
+ // POST /webhooks
50
+ // ---------------------------------------------------------------------------
51
+ export const webhookCreateRequestSchema = z.strictObject({
52
+ environment_id: environmentIdSchema,
53
+ url: httpsUrlSchema,
54
+ event_types: z.array(webhookEventTypeSchema).min(1),
55
+ });
56
+ /** One-time plaintext signing secret (design.md §11.12's display-once rule, applied here too; §29.4 HMAC). */
57
+ export const webhookCreateResponseSchema = webhookSchema.extend({
58
+ secret: z.string().min(1),
59
+ });
60
+ // ---------------------------------------------------------------------------
61
+ // PATCH /webhooks/:webhookId
62
+ // ---------------------------------------------------------------------------
63
+ export const webhookUpdateRequestSchema = z.strictObject({
64
+ url: httpsUrlSchema.optional(),
65
+ event_types: z.array(webhookEventTypeSchema).min(1).optional(),
66
+ status: webhookStatusSchema.optional(),
67
+ });
68
+ export const webhookUpdateResponseSchema = webhookSchema;
69
+ // ---------------------------------------------------------------------------
70
+ // DELETE /webhooks/:webhookId
71
+ // ---------------------------------------------------------------------------
72
+ export const webhookDeleteResponseSchema = z.strictObject({
73
+ id: webhookIdSchema,
74
+ deleted: z.literal(true),
75
+ });
76
+ // ---------------------------------------------------------------------------
77
+ // POST /webhooks/:webhookId/rotate-secret
78
+ // ---------------------------------------------------------------------------
79
+ export const webhookRotateSecretRequestSchema = z.strictObject({});
80
+ export const webhookRotateSecretResponseSchema = z.strictObject({
81
+ id: webhookIdSchema,
82
+ secret: z.string().min(1),
83
+ rotated_at: z.iso.datetime(),
84
+ });
85
+ // ---------------------------------------------------------------------------
86
+ // POST /webhooks/:webhookId/test
87
+ // ---------------------------------------------------------------------------
88
+ export const webhookTestRequestSchema = z.strictObject({
89
+ /** Defaults to a synthetic event if omitted; must be one of this webhook's subscribed `event_types`. */
90
+ event_type: webhookEventTypeSchema.optional(),
91
+ });
92
+ /** design.md §29.4: "2xxだけ成功". `delivered` reports whether the endpoint returned 2xx. */
93
+ export const webhookTestResponseSchema = z.strictObject({
94
+ delivered: z.boolean(),
95
+ response_status: z.number().int().min(100).max(599).nullable(),
96
+ response_time_ms: z.number().int().nonnegative().nullable(),
97
+ requested_at: z.iso.datetime(),
98
+ });
99
+ // ---------------------------------------------------------------------------
100
+ // GET /webhooks/:webhookId/deliveries
101
+ // ---------------------------------------------------------------------------
102
+ /**
103
+ * `webhook_delivery_history` (design.md §29.4, §10.10): one row per delivery
104
+ * attempt. Deliberately excludes any request/response body, header, or
105
+ * signature — only status/attempt/response code/duration are ever recorded
106
+ * or exposed here, matching `@smartcrab/database-control`'s
107
+ * `WebhookDeliveryHistoryRecord`.
108
+ */
109
+ export const WEBHOOK_DELIVERY_STATUSES = ["pending", "delivered", "failed", "exhausted"];
110
+ export const webhookDeliveryStatusSchema = z.enum(WEBHOOK_DELIVERY_STATUSES);
111
+ export const webhookDeliverySchema = z.strictObject({
112
+ id: deliveryIdSchema,
113
+ webhook_id: webhookIdSchema,
114
+ event_id: eventIdSchema,
115
+ /**
116
+ * Not `webhookEventTypeSchema` (that closed enum belongs to a webhook's
117
+ * *subscription* list on `webhookSchema` above): history rows are retained
118
+ * 90 days (design.md §10.10) and must stay parseable even if the event
119
+ * catalog changes after a row was written, so this is a plain non-empty
120
+ * string.
121
+ */
122
+ event_type: z.string().min(1),
123
+ attempt: z.number().int().positive(),
124
+ status: webhookDeliveryStatusSchema,
125
+ response_status_code: z.number().int().min(100).max(599).nullable(),
126
+ duration_ms: z.number().int().nonnegative(),
127
+ occurred_at: z.iso.datetime(),
128
+ });
129
+ export const webhookDeliveryListQuerySchema = paginationQuerySchema.extend({
130
+ status: webhookDeliveryStatusSchema.optional(),
131
+ occurred_after: z.iso.datetime().optional(),
132
+ occurred_before: z.iso.datetime().optional(),
133
+ });
134
+ export const webhookDeliveryListResponseSchema = cursorPageSchema(webhookDeliverySchema);
@@ -0,0 +1,172 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * `workspaces` (design.md §11.1, §7.1). No secret/internal-routing columns
4
+ * exist on this table to exclude; `stripe_customer_id` is an opaque Stripe
5
+ * identifier (not a secret — it carries no ability to act on the customer)
6
+ * and is useful to dashboard/billing UI, so it is exposed as nullable.
7
+ */
8
+ export declare const WORKSPACE_STATUSES: readonly ["active", "past_due", "suspended", "deleted"];
9
+ export declare const workspaceStatusSchema: z.ZodEnum<{
10
+ active: "active";
11
+ deleted: "deleted";
12
+ suspended: "suspended";
13
+ past_due: "past_due";
14
+ }>;
15
+ export type WorkspaceStatus = (typeof WORKSPACE_STATUSES)[number];
16
+ export declare const workspaceSchema: z.ZodObject<{
17
+ id: z.ZodString;
18
+ name: z.ZodString;
19
+ slug: z.ZodString;
20
+ status: z.ZodEnum<{
21
+ active: "active";
22
+ deleted: "deleted";
23
+ suspended: "suspended";
24
+ past_due: "past_due";
25
+ }>;
26
+ stripe_customer_id: z.ZodNullable<z.ZodString>;
27
+ created_at: z.ZodISODateTime;
28
+ updated_at: z.ZodISODateTime;
29
+ deleted_at: z.ZodNullable<z.ZodISODateTime>;
30
+ }, z.core.$strict>;
31
+ export type Workspace = z.infer<typeof workspaceSchema>;
32
+ export declare const workspaceListQuerySchema: z.ZodObject<{
33
+ cursor: z.ZodOptional<z.ZodString>;
34
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
35
+ status: z.ZodOptional<z.ZodEnum<{
36
+ active: "active";
37
+ deleted: "deleted";
38
+ suspended: "suspended";
39
+ past_due: "past_due";
40
+ }>>;
41
+ }, z.core.$strip>;
42
+ export type WorkspaceListQuery = z.infer<typeof workspaceListQuerySchema>;
43
+ export declare const workspaceListResponseSchema: z.ZodObject<{
44
+ items: z.ZodArray<z.ZodObject<{
45
+ id: z.ZodString;
46
+ name: z.ZodString;
47
+ slug: z.ZodString;
48
+ status: z.ZodEnum<{
49
+ active: "active";
50
+ deleted: "deleted";
51
+ suspended: "suspended";
52
+ past_due: "past_due";
53
+ }>;
54
+ stripe_customer_id: z.ZodNullable<z.ZodString>;
55
+ created_at: z.ZodISODateTime;
56
+ updated_at: z.ZodISODateTime;
57
+ deleted_at: z.ZodNullable<z.ZodISODateTime>;
58
+ }, z.core.$strict>>;
59
+ next_cursor: z.ZodNullable<z.ZodString>;
60
+ }, z.core.$strict>;
61
+ export type WorkspaceListResponse = z.infer<typeof workspaceListResponseSchema>;
62
+ /** `slug` is optional: when omitted, the server derives one from `name` (design.md §11.1 `slug UNIQUE`). */
63
+ export declare const workspaceCreateRequestSchema: z.ZodObject<{
64
+ name: z.ZodString;
65
+ slug: z.ZodOptional<z.ZodString>;
66
+ }, z.core.$strict>;
67
+ export type WorkspaceCreateRequest = z.infer<typeof workspaceCreateRequestSchema>;
68
+ export declare const workspaceCreateResponseSchema: z.ZodObject<{
69
+ id: z.ZodString;
70
+ name: z.ZodString;
71
+ slug: z.ZodString;
72
+ status: z.ZodEnum<{
73
+ active: "active";
74
+ deleted: "deleted";
75
+ suspended: "suspended";
76
+ past_due: "past_due";
77
+ }>;
78
+ stripe_customer_id: z.ZodNullable<z.ZodString>;
79
+ created_at: z.ZodISODateTime;
80
+ updated_at: z.ZodISODateTime;
81
+ deleted_at: z.ZodNullable<z.ZodISODateTime>;
82
+ }, z.core.$strict>;
83
+ export type WorkspaceCreateResponse = z.infer<typeof workspaceCreateResponseSchema>;
84
+ export declare const workspaceGetResponseSchema: z.ZodObject<{
85
+ id: z.ZodString;
86
+ name: z.ZodString;
87
+ slug: z.ZodString;
88
+ status: z.ZodEnum<{
89
+ active: "active";
90
+ deleted: "deleted";
91
+ suspended: "suspended";
92
+ past_due: "past_due";
93
+ }>;
94
+ stripe_customer_id: z.ZodNullable<z.ZodString>;
95
+ created_at: z.ZodISODateTime;
96
+ updated_at: z.ZodISODateTime;
97
+ deleted_at: z.ZodNullable<z.ZodISODateTime>;
98
+ }, z.core.$strict>;
99
+ export type WorkspaceGetResponse = z.infer<typeof workspaceGetResponseSchema>;
100
+ /**
101
+ * `status` is deliberately excluded: workspace suspension/deletion are
102
+ * billing- and Owner-driven lifecycle transitions (design.md §27.2 "Owner: 全
103
+ * 操作、課金、Workspace削除"), not a generic field patch.
104
+ */
105
+ export declare const workspaceUpdateRequestSchema: z.ZodObject<{
106
+ name: z.ZodOptional<z.ZodString>;
107
+ slug: z.ZodOptional<z.ZodString>;
108
+ }, z.core.$strict>;
109
+ export type WorkspaceUpdateRequest = z.infer<typeof workspaceUpdateRequestSchema>;
110
+ export declare const workspaceUpdateResponseSchema: z.ZodObject<{
111
+ id: z.ZodString;
112
+ name: z.ZodString;
113
+ slug: z.ZodString;
114
+ status: z.ZodEnum<{
115
+ active: "active";
116
+ deleted: "deleted";
117
+ suspended: "suspended";
118
+ past_due: "past_due";
119
+ }>;
120
+ stripe_customer_id: z.ZodNullable<z.ZodString>;
121
+ created_at: z.ZodISODateTime;
122
+ updated_at: z.ZodISODateTime;
123
+ deleted_at: z.ZodNullable<z.ZodISODateTime>;
124
+ }, z.core.$strict>;
125
+ export type WorkspaceUpdateResponse = z.infer<typeof workspaceUpdateResponseSchema>;
126
+ /**
127
+ * `workspace_members` (design.md §11.2, §27.2). Exposes only the fields the
128
+ * assignment enumerates — `platform_user_id`/`role`/`created_at`/
129
+ * `updated_at` — no credential or session material (this table carries none
130
+ * anyway; those four columns are its entire shape). `platform_user_id` is a
131
+ * plain non-empty string rather than `userIdSchema`: it identifies a
132
+ * platform (dashboard) user of this Control Plane's own dogfooded
133
+ * Environment (design.md §27.1), a distinct identifier space from the
134
+ * `usr_...`-pattern end users `userIdSchema` models elsewhere in this
135
+ * package.
136
+ */
137
+ export declare const workspaceMemberSchema: z.ZodObject<{
138
+ platform_user_id: z.ZodString;
139
+ role: z.ZodEnum<{
140
+ owner: "owner";
141
+ admin: "admin";
142
+ developer: "developer";
143
+ support: "support";
144
+ billing: "billing";
145
+ viewer: "viewer";
146
+ }>;
147
+ created_at: z.ZodISODateTime;
148
+ updated_at: z.ZodISODateTime;
149
+ }, z.core.$strict>;
150
+ export type WorkspaceMember = z.infer<typeof workspaceMemberSchema>;
151
+ export declare const workspaceMemberListQuerySchema: z.ZodObject<{
152
+ cursor: z.ZodOptional<z.ZodString>;
153
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
154
+ }, z.core.$strip>;
155
+ export type WorkspaceMemberListQuery = z.infer<typeof workspaceMemberListQuerySchema>;
156
+ export declare const workspaceMemberListResponseSchema: z.ZodObject<{
157
+ items: z.ZodArray<z.ZodObject<{
158
+ platform_user_id: z.ZodString;
159
+ role: z.ZodEnum<{
160
+ owner: "owner";
161
+ admin: "admin";
162
+ developer: "developer";
163
+ support: "support";
164
+ billing: "billing";
165
+ viewer: "viewer";
166
+ }>;
167
+ created_at: z.ZodISODateTime;
168
+ updated_at: z.ZodISODateTime;
169
+ }, z.core.$strict>>;
170
+ next_cursor: z.ZodNullable<z.ZodString>;
171
+ }, z.core.$strict>;
172
+ export type WorkspaceMemberListResponse = z.infer<typeof workspaceMemberListResponseSchema>;
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import { cursorPageSchema, paginationQuerySchema } from "./common.js";
3
+ import { slugSchema, workspaceIdSchema } from "./primitives.js";
4
+ import { workspaceRoleSchema } from "./rbac.js";
5
+ /**
6
+ * `workspaces` (design.md §11.1, §7.1). No secret/internal-routing columns
7
+ * exist on this table to exclude; `stripe_customer_id` is an opaque Stripe
8
+ * identifier (not a secret — it carries no ability to act on the customer)
9
+ * and is useful to dashboard/billing UI, so it is exposed as nullable.
10
+ */
11
+ export const WORKSPACE_STATUSES = ["active", "past_due", "suspended", "deleted"];
12
+ export const workspaceStatusSchema = z.enum(WORKSPACE_STATUSES);
13
+ export const workspaceSchema = z.strictObject({
14
+ id: workspaceIdSchema,
15
+ name: z.string().min(1).max(200),
16
+ slug: slugSchema,
17
+ status: workspaceStatusSchema,
18
+ stripe_customer_id: z.string().min(1).nullable(),
19
+ created_at: z.iso.datetime(),
20
+ updated_at: z.iso.datetime(),
21
+ deleted_at: z.iso.datetime().nullable(),
22
+ });
23
+ // ---------------------------------------------------------------------------
24
+ // GET /workspaces
25
+ // ---------------------------------------------------------------------------
26
+ export const workspaceListQuerySchema = paginationQuerySchema.extend({
27
+ status: workspaceStatusSchema.optional(),
28
+ });
29
+ export const workspaceListResponseSchema = cursorPageSchema(workspaceSchema);
30
+ // ---------------------------------------------------------------------------
31
+ // POST /workspaces
32
+ // ---------------------------------------------------------------------------
33
+ /** `slug` is optional: when omitted, the server derives one from `name` (design.md §11.1 `slug UNIQUE`). */
34
+ export const workspaceCreateRequestSchema = z.strictObject({
35
+ name: z.string().min(1).max(200),
36
+ slug: slugSchema.optional(),
37
+ });
38
+ export const workspaceCreateResponseSchema = workspaceSchema;
39
+ // ---------------------------------------------------------------------------
40
+ // GET /workspaces/:workspaceId
41
+ // ---------------------------------------------------------------------------
42
+ export const workspaceGetResponseSchema = workspaceSchema;
43
+ // ---------------------------------------------------------------------------
44
+ // PATCH /workspaces/:workspaceId
45
+ // ---------------------------------------------------------------------------
46
+ /**
47
+ * `status` is deliberately excluded: workspace suspension/deletion are
48
+ * billing- and Owner-driven lifecycle transitions (design.md §27.2 "Owner: 全
49
+ * 操作、課金、Workspace削除"), not a generic field patch.
50
+ */
51
+ export const workspaceUpdateRequestSchema = z.strictObject({
52
+ name: z.string().min(1).max(200).optional(),
53
+ slug: slugSchema.optional(),
54
+ });
55
+ export const workspaceUpdateResponseSchema = workspaceSchema;
56
+ // ---------------------------------------------------------------------------
57
+ // GET /workspaces/:workspaceId/members
58
+ // ---------------------------------------------------------------------------
59
+ /**
60
+ * `workspace_members` (design.md §11.2, §27.2). Exposes only the fields the
61
+ * assignment enumerates — `platform_user_id`/`role`/`created_at`/
62
+ * `updated_at` — no credential or session material (this table carries none
63
+ * anyway; those four columns are its entire shape). `platform_user_id` is a
64
+ * plain non-empty string rather than `userIdSchema`: it identifies a
65
+ * platform (dashboard) user of this Control Plane's own dogfooded
66
+ * Environment (design.md §27.1), a distinct identifier space from the
67
+ * `usr_...`-pattern end users `userIdSchema` models elsewhere in this
68
+ * package.
69
+ */
70
+ export const workspaceMemberSchema = z.strictObject({
71
+ platform_user_id: z.string().min(1),
72
+ role: workspaceRoleSchema,
73
+ created_at: z.iso.datetime(),
74
+ updated_at: z.iso.datetime(),
75
+ });
76
+ export const workspaceMemberListQuerySchema = paginationQuerySchema;
77
+ export const workspaceMemberListResponseSchema = cursorPageSchema(workspaceMemberSchema);
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@smartcrab/contracts-management",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "development": "./src/index.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./*": {
16
+ "types": "./src/*.ts",
17
+ "development": "./src/*.ts",
18
+ "import": "./dist/*.js",
19
+ "default": "./dist/*.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "zod": "4.4.3",
31
+ "@smartcrab/contracts-public": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "5.9.3",
35
+ "vitest": "4.1.11"
36
+ },
37
+ "scripts": {
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run --passWithNoTests",
40
+ "build": "tsc -p tsconfig.build.json"
41
+ }
42
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ managementApiKeyCreateRequestSchema,
4
+ managementApiKeyCreateResponseSchema,
5
+ managementApiKeyListResponseSchema,
6
+ managementApiKeySchema,
7
+ } from "./api-keys.js";
8
+
9
+ const VALID_KEY = {
10
+ id: `mky_${"a".repeat(32)}`,
11
+ workspace_id: `wsp_${"a".repeat(32)}`,
12
+ environment_id: null,
13
+ name: "CI deploy key",
14
+ prefix: "sk_live_ab12",
15
+ scopes: ["clients:read", "clients:write"],
16
+ last_used_at: null,
17
+ expires_at: null,
18
+ revoked_at: null,
19
+ created_by: `usr_${"a".repeat(32)}`,
20
+ created_at: "2026-08-02T08:10:00Z",
21
+ };
22
+
23
+ describe("managementApiKeySchema (design.md §11.12) never exposes key_mac", () => {
24
+ test("accepts a valid key", () => {
25
+ expect(managementApiKeySchema.safeParse(VALID_KEY).success).toBe(true);
26
+ });
27
+
28
+ test("rejects an unknown top-level key (e.g. leaking key_mac)", () => {
29
+ expect(managementApiKeySchema.safeParse({ ...VALID_KEY, key_mac: "abc" }).success).toBe(false);
30
+ });
31
+
32
+ test("rejects an unknown scope", () => {
33
+ expect(
34
+ managementApiKeySchema.safeParse({ ...VALID_KEY, scopes: ["not_a_scope"] }).success,
35
+ ).toBe(false);
36
+ });
37
+ });
38
+
39
+ describe("management API key create response includes the one-time secret, list does not", () => {
40
+ test("create response requires a well-formed sk_live_/sk_test_ secret (design.md §26.1)", () => {
41
+ expect(
42
+ managementApiKeyCreateResponseSchema.safeParse({
43
+ ...VALID_KEY,
44
+ secret: `sk_live_${"a".repeat(24)}`,
45
+ }).success,
46
+ ).toBe(true);
47
+ expect(
48
+ managementApiKeyCreateResponseSchema.safeParse({
49
+ ...VALID_KEY,
50
+ secret: "not-the-right-format",
51
+ }).success,
52
+ ).toBe(false);
53
+ });
54
+
55
+ test("list response rejects an item carrying a secret", () => {
56
+ expect(
57
+ managementApiKeyListResponseSchema.safeParse({
58
+ items: [{ ...VALID_KEY, secret: `sk_live_${"a".repeat(24)}` }],
59
+ next_cursor: null,
60
+ }).success,
61
+ ).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe("managementApiKeyCreateRequestSchema", () => {
66
+ test("requires at least one scope (design.md §26.1 'scope最小化')", () => {
67
+ expect(
68
+ managementApiKeyCreateRequestSchema.safeParse({
69
+ workspace_id: VALID_KEY.workspace_id,
70
+ name: "Empty scopes",
71
+ scopes: [],
72
+ }).success,
73
+ ).toBe(false);
74
+ });
75
+ });
76
+ describe("Management API key token introspection scope", () => {
77
+ test("allows an introspection-only key", () => {
78
+ const parsed = managementApiKeyCreateRequestSchema.safeParse({
79
+ workspace_id: VALID_KEY.workspace_id,
80
+ name: "Resource server introspection",
81
+ scopes: ["introspect"],
82
+ });
83
+ expect(parsed.success).toBe(true);
84
+ if (parsed.success) expect(parsed.data.scopes).toEqual(["introspect"]);
85
+ });
86
+ });
@@ -0,0 +1,97 @@
1
+ import { z } from "zod";
2
+ import { cursorPageSchema, paginationQuerySchema } from "./common.js";
3
+ import { apiKeyIdSchema, environmentIdSchema, workspaceIdSchema } from "./primitives.js";
4
+ import { MANAGEMENT_SCOPES, managementScopeSchema } from "./rbac.js";
5
+
6
+ /**
7
+ * `management_api_keys` (design.md §11.12, §26.1).
8
+ *
9
+ * DEVIATION: design.md §26.2's route list does not enumerate `/api-keys`
10
+ * routes explicitly, but §26.1 ("Management API key", "scope最小化"),
11
+ * §11.12 (the table + "Secretは作成時に1回だけ表示する"), and this
12
+ * assignment's requirement 4 all describe the resource, so it is modeled
13
+ * here the same way every other enumerated resource is (list/create + a
14
+ * revoke action), following the create/rotate-secret pattern used for
15
+ * `clients` and `webhooks`.
16
+ *
17
+ * `key_mac` (the forbidden HMAC-of-the-full-key column) is excluded from
18
+ * every schema below; `prefix` is kept (it exists specifically to be a safe,
19
+ * non-secret display fragment, design.md §11.12).
20
+ */
21
+
22
+ /** design.md §26.1: `Authorization: Bearer sk_live_...` / test-mode keys use `sk_test_`. */
23
+ export const MANAGEMENT_API_KEY_LIVE_PREFIX = "sk_live_";
24
+ export const MANAGEMENT_API_KEY_TEST_PREFIX = "sk_test_";
25
+
26
+ /**
27
+ * Length of the non-secret display prefix stored in `management_api_keys.key_prefix`
28
+ * (`sk_live_` / `sk_test_` + the first 4 opaque chars). Prefix lookup slices the
29
+ * presented bearer secret to exactly this many characters. Both mode literals are
30
+ * the same length, so one value covers live and test keys.
31
+ */
32
+ export const MANAGEMENT_API_KEY_PREFIX_LENGTH = MANAGEMENT_API_KEY_LIVE_PREFIX.length + 4;
33
+
34
+ /** The full bearer secret's wire format: `sk_(live|test)_<opaque>`. Shown only on create/rotate. */
35
+ export const managementApiKeySecretSchema = z
36
+ .string()
37
+ .regex(/^sk_(?:live|test)_[A-Za-z0-9_-]{20,}$/);
38
+
39
+ export const managementApiKeySchema = z.strictObject({
40
+ id: apiKeyIdSchema,
41
+ workspace_id: workspaceIdSchema,
42
+ /** design.md §11.12 `environment_id NULLABLE`: a workspace-wide key when absent. */
43
+ environment_id: environmentIdSchema.nullable(),
44
+ name: z.string().min(1).max(200),
45
+ /** The non-secret display prefix, e.g. `sk_live_ab12`. */
46
+ prefix: z.string().min(1),
47
+ scopes: z.array(managementScopeSchema).min(1),
48
+ last_used_at: z.iso.datetime().nullable(),
49
+ expires_at: z.iso.datetime().nullable(),
50
+ revoked_at: z.iso.datetime().nullable(),
51
+ created_by: z.string().min(1),
52
+ created_at: z.iso.datetime(),
53
+ });
54
+ export type ManagementApiKey = z.infer<typeof managementApiKeySchema>;
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // GET /api-keys
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export const managementApiKeyListQuerySchema = paginationQuerySchema.extend({
61
+ workspace_id: workspaceIdSchema,
62
+ environment_id: environmentIdSchema.optional(),
63
+ });
64
+ export type ManagementApiKeyListQuery = z.infer<typeof managementApiKeyListQuerySchema>;
65
+
66
+ export const managementApiKeyListResponseSchema = cursorPageSchema(managementApiKeySchema);
67
+ export type ManagementApiKeyListResponse = z.infer<typeof managementApiKeyListResponseSchema>;
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // POST /api-keys
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export const managementApiKeyCreateRequestSchema = z.strictObject({
74
+ workspace_id: workspaceIdSchema,
75
+ name: z.string().min(1).max(200),
76
+ /** design.md §26.1 "scope最小化": callers must explicitly enumerate at least one scope. */
77
+ scopes: z.array(managementScopeSchema).min(1).max(MANAGEMENT_SCOPES.length),
78
+ environment_id: environmentIdSchema.optional(),
79
+ expires_at: z.iso.datetime().optional(),
80
+ });
81
+ export type ManagementApiKeyCreateRequest = z.infer<typeof managementApiKeyCreateRequestSchema>;
82
+
83
+ /** design.md §11.12: "Secretは作成時に1回だけ表示する。" `secret` never appears again after this response. */
84
+ export const managementApiKeyCreateResponseSchema = managementApiKeySchema.extend({
85
+ secret: managementApiKeySecretSchema,
86
+ });
87
+ export type ManagementApiKeyCreateResponse = z.infer<typeof managementApiKeyCreateResponseSchema>;
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Revoke (DELETE-shaped action; not in the §26.2 route list — see DEVIATION above)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export const managementApiKeyRevokeResponseSchema = z.strictObject({
94
+ id: apiKeyIdSchema,
95
+ revoked: z.literal(true),
96
+ });
97
+ export type ManagementApiKeyRevokeResponse = z.infer<typeof managementApiKeyRevokeResponseSchema>;
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { auditEventListQuerySchema, auditEventSchema } from "./audit-events.js";
3
+
4
+ const VALID_EVENT = {
5
+ id: `evt_${"a".repeat(32)}`,
6
+ environment_id: `env_${"a".repeat(32)}`,
7
+ workspace_id: `wsp_${"a".repeat(32)}`,
8
+ actor_type: "workspace_member",
9
+ actor_id: `usr_${"a".repeat(32)}`,
10
+ action: "client.updated",
11
+ subject_type: "client",
12
+ subject_id: `cli_${"a".repeat(32)}`,
13
+ result: "success",
14
+ request_id: `req_${"a".repeat(32)}`,
15
+ metadata: {},
16
+ created_at: "2026-08-02T08:10:00Z",
17
+ };
18
+
19
+ describe("auditEventSchema (design.md §12.11)", () => {
20
+ test("accepts a valid audit event", () => {
21
+ expect(auditEventSchema.safeParse(VALID_EVENT).success).toBe(true);
22
+ });
23
+
24
+ test("rejects an unknown top-level key (e.g. leaking auth_partition_id)", () => {
25
+ expect(auditEventSchema.safeParse({ ...VALID_EVENT, auth_partition_id: "prt_1" }).success).toBe(
26
+ false,
27
+ );
28
+ });
29
+
30
+ test("rejects an unknown actor_type", () => {
31
+ expect(auditEventSchema.safeParse({ ...VALID_EVENT, actor_type: "robot" }).success).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe("auditEventListQuerySchema pagination", () => {
36
+ test("rejects a page size above the max of 100", () => {
37
+ expect(
38
+ auditEventListQuerySchema.safeParse({
39
+ environment_id: VALID_EVENT.environment_id,
40
+ limit: 500,
41
+ }).success,
42
+ ).toBe(false);
43
+ });
44
+ });