@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.
- package/LICENSE +202 -0
- package/dist/api-keys.d.ts +251 -0
- package/dist/api-keys.js +78 -0
- package/dist/audit-events.d.ts +94 -0
- package/dist/audit-events.js +45 -0
- package/dist/billing.d.ts +30 -0
- package/dist/billing.js +30 -0
- package/dist/clients.d.ts +328 -0
- package/dist/clients.js +141 -0
- package/dist/common.d.ts +95 -0
- package/dist/common.js +99 -0
- package/dist/connections.d.ts +201 -0
- package/dist/connections.js +107 -0
- package/dist/domains.d.ts +167 -0
- package/dist/domains.js +74 -0
- package/dist/environments.d.ts +194 -0
- package/dist/environments.js +101 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/primitives.d.ts +49 -0
- package/dist/primitives.js +56 -0
- package/dist/projects.d.ts +78 -0
- package/dist/projects.js +49 -0
- package/dist/rbac.d.ts +222 -0
- package/dist/rbac.js +226 -0
- package/dist/usage.d.ts +72 -0
- package/dist/usage.js +52 -0
- package/dist/user-jobs.d.ts +253 -0
- package/dist/user-jobs.js +95 -0
- package/dist/user-subresources.d.ts +161 -0
- package/dist/user-subresources.js +85 -0
- package/dist/users.d.ts +193 -0
- package/dist/users.js +112 -0
- package/dist/webhooks.d.ts +306 -0
- package/dist/webhooks.js +134 -0
- package/dist/workspaces.d.ts +172 -0
- package/dist/workspaces.js +77 -0
- package/package.json +42 -0
- package/src/api-keys.test.ts +86 -0
- package/src/api-keys.ts +97 -0
- package/src/audit-events.test.ts +44 -0
- package/src/audit-events.ts +57 -0
- package/src/billing.test.ts +57 -0
- package/src/billing.ts +46 -0
- package/src/clients.test.ts +169 -0
- package/src/clients.ts +182 -0
- package/src/common.test.ts +102 -0
- package/src/common.ts +127 -0
- package/src/connections.test.ts +65 -0
- package/src/connections.ts +134 -0
- package/src/domains.test.ts +48 -0
- package/src/domains.ts +104 -0
- package/src/environments.test.ts +67 -0
- package/src/environments.ts +133 -0
- package/src/index.ts +27 -0
- package/src/primitives.ts +100 -0
- package/src/projects.test.ts +49 -0
- package/src/projects.ts +72 -0
- package/src/rbac.test.ts +148 -0
- package/src/rbac.ts +250 -0
- package/src/secret-fields.test.ts +84 -0
- package/src/usage.test.ts +57 -0
- package/src/usage.ts +66 -0
- package/src/user-jobs.test.ts +86 -0
- package/src/user-jobs.ts +120 -0
- package/src/user-subresources.test.ts +115 -0
- package/src/user-subresources.ts +128 -0
- package/src/users.test.ts +91 -0
- package/src/users.ts +149 -0
- package/src/webhooks.test.ts +187 -0
- package/src/webhooks.ts +182 -0
- package/src/workspaces.test.ts +127 -0
- package/src/workspaces.ts +106 -0
package/dist/webhooks.js
ADDED
|
@@ -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
|
+
});
|
package/src/api-keys.ts
ADDED
|
@@ -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
|
+
});
|