@01.software/cli 0.7.1 → 0.8.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/dist/index.js +2 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp/{chunk-3ZSKJM43.js → chunk-45ZCPS57.js} +340 -292
- package/dist/mcp/chunk-45ZCPS57.js.map +1 -0
- package/dist/mcp/http.js +261 -120
- package/dist/mcp/http.js.map +1 -1
- package/dist/mcp/stdio.js +1 -1
- package/dist/mcp/vercel.js +602 -415
- package/package.json +3 -3
- package/dist/mcp/chunk-3ZSKJM43.js.map +0 -1
package/dist/mcp/vercel.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/handler.ts
|
|
2
2
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import {
|
|
4
|
+
MCP_OAUTH_ISSUER as MCP_OAUTH_ISSUER3,
|
|
5
|
+
MCP_PROTECTED_RESOURCE_METADATA_PATH,
|
|
6
|
+
MCP_RESOURCE_AUDIENCE as MCP_RESOURCE_AUDIENCE2,
|
|
7
|
+
MCP_SCOPES as MCP_SCOPES2
|
|
8
|
+
} from "@01.software/auth-contracts";
|
|
3
9
|
|
|
4
10
|
// src/server.ts
|
|
5
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -10,40 +16,207 @@ import { z } from "zod";
|
|
|
10
16
|
// src/lib/request-context.ts
|
|
11
17
|
import { AsyncLocalStorage } from "async_hooks";
|
|
12
18
|
var requestContext = new AsyncLocalStorage();
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
function tenantAuthContext() {
|
|
20
|
+
return requestContext.getStore()?.auth ?? null;
|
|
21
|
+
}
|
|
22
|
+
function hasRequestContext() {
|
|
23
|
+
return requestContext.getStore() !== void 0;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
// src/lib/client.ts
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
import {
|
|
28
|
+
CollectionClient,
|
|
29
|
+
CommunityClient,
|
|
30
|
+
ModerationApi,
|
|
31
|
+
ServerCommerceClient,
|
|
32
|
+
createServerClient
|
|
33
|
+
} from "@01.software/sdk";
|
|
34
|
+
|
|
35
|
+
// src/service-auth.ts
|
|
36
|
+
import { createPrivateKey, randomUUID, sign as signBytes } from "crypto";
|
|
37
|
+
import {
|
|
38
|
+
MCP_CONSOLE_SERVICE_AUDIENCE,
|
|
39
|
+
MCP_CONSOLE_SERVICE_SCOPE,
|
|
40
|
+
MCP_OAUTH_ISSUER,
|
|
41
|
+
MCP_SERVICE_TOKEN_LIFETIME_SECONDS,
|
|
42
|
+
MCP_TENANT_CLAIM,
|
|
43
|
+
MCP_TENANT_ROLE_CLAIM
|
|
44
|
+
} from "@01.software/auth-contracts";
|
|
45
|
+
var KEYSET_ENV = "MCP_SERVICE_KEYSET";
|
|
46
|
+
function assertProductionKeysetUse(source) {
|
|
47
|
+
const vercelEnv = process.env.VERCEL_ENV;
|
|
48
|
+
if (vercelEnv && vercelEnv !== "production") {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${source} is only allowed in production Vercel deployments; non-production MCP service auth needs environment-specific issuer, audience, JWKS URI, and key material`
|
|
51
|
+
);
|
|
29
52
|
}
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
}
|
|
54
|
+
function parsePrivateJwk() {
|
|
55
|
+
const keyset = signingKeyset();
|
|
56
|
+
const jwk = keyset.current;
|
|
57
|
+
const source = keyset.source;
|
|
58
|
+
if (typeof jwk.d !== "string" || jwk.d.length === 0) {
|
|
59
|
+
throw new Error(`${source} current key must be a private JWK`);
|
|
32
60
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
61
|
+
if (typeof jwk.kid !== "string" || jwk.kid.length === 0) {
|
|
62
|
+
throw new Error(`${source} must include kid`);
|
|
63
|
+
}
|
|
64
|
+
return jwk;
|
|
65
|
+
}
|
|
66
|
+
function signingKeyset() {
|
|
67
|
+
const raw = process.env[KEYSET_ENV];
|
|
68
|
+
const source = KEYSET_ENV;
|
|
69
|
+
if (raw) assertProductionKeysetUse(source);
|
|
70
|
+
const parsed = (() => {
|
|
71
|
+
if (!raw) return null;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error(`${KEYSET_ENV} is invalid JSON`);
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
if (!parsed) throw new Error("MCP service JWT signing key is not configured");
|
|
79
|
+
const keys = Array.isArray(parsed.keys) ? parsed.keys : [parsed];
|
|
80
|
+
if (keys.length === 0 || keys.length > 2) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${source} must contain one current key and at most one previous key`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const currentKid = parsed.current_kid;
|
|
86
|
+
if (typeof currentKid !== "string" && keys.length > 1) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${source} must include current_kid when multiple keys are present`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const current = typeof currentKid === "string" ? keys.find((key) => key.kid === currentKid) : keys[0];
|
|
92
|
+
if (!current) throw new Error(`${source} current_kid is not in keys`);
|
|
93
|
+
return { current, keys, source };
|
|
94
|
+
}
|
|
95
|
+
function algForJwk(jwk) {
|
|
96
|
+
if (jwk.kty === "RSA") return "RS256";
|
|
97
|
+
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ES256";
|
|
98
|
+
throw new Error("MCP service JWT signing key must be RSA or P-256 EC");
|
|
99
|
+
}
|
|
100
|
+
function toPublicJwk(jwk) {
|
|
101
|
+
const {
|
|
102
|
+
d: _d,
|
|
103
|
+
p: _p,
|
|
104
|
+
q: _q,
|
|
105
|
+
dp: _dp,
|
|
106
|
+
dq: _dq,
|
|
107
|
+
qi: _qi,
|
|
108
|
+
oth: _oth,
|
|
109
|
+
...publicJwk
|
|
110
|
+
} = jwk;
|
|
111
|
+
return {
|
|
112
|
+
...publicJwk,
|
|
113
|
+
alg: typeof publicJwk.alg === "string" ? publicJwk.alg : algForJwk(jwk),
|
|
114
|
+
use: "sig"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function base64urlJson(value) {
|
|
118
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
119
|
+
}
|
|
120
|
+
function apiScopesFor(context) {
|
|
121
|
+
return context.scopes.includes("mcp:write") ? ["read", "write"] : ["read"];
|
|
122
|
+
}
|
|
123
|
+
function mcpServicePublicJwks() {
|
|
124
|
+
const keyset = signingKeyset();
|
|
125
|
+
const keys = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const jwk of keyset.keys.map(toPublicJwk)) {
|
|
127
|
+
if (typeof jwk.kid === "string" && jwk.kid.length > 0) {
|
|
128
|
+
keys.set(jwk.kid, jwk);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { keys: [...keys.values()] };
|
|
132
|
+
}
|
|
133
|
+
function signMcpServiceToken(context) {
|
|
134
|
+
if (!context.principalId) {
|
|
135
|
+
throw new Error("MCP OAuth principal is required for Console service auth");
|
|
136
|
+
}
|
|
137
|
+
const jwk = parsePrivateJwk();
|
|
138
|
+
const alg = algForJwk(jwk);
|
|
139
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
140
|
+
const payload = {
|
|
141
|
+
iss: MCP_OAUTH_ISSUER,
|
|
142
|
+
aud: MCP_CONSOLE_SERVICE_AUDIENCE,
|
|
143
|
+
iat: now,
|
|
144
|
+
nbf: now,
|
|
145
|
+
exp: now + MCP_SERVICE_TOKEN_LIFETIME_SECONDS,
|
|
146
|
+
jti: randomUUID(),
|
|
147
|
+
sub: context.principalId,
|
|
148
|
+
act: {
|
|
149
|
+
sub: context.principalId,
|
|
150
|
+
tenant_id: context.tenantId
|
|
151
|
+
},
|
|
152
|
+
[MCP_TENANT_CLAIM]: context.tenantId,
|
|
153
|
+
[MCP_TENANT_ROLE_CLAIM]: context.tenantRole,
|
|
154
|
+
scope: MCP_CONSOLE_SERVICE_SCOPE,
|
|
155
|
+
api_scopes: apiScopesFor(context),
|
|
156
|
+
mcp_scopes: context.scopes
|
|
157
|
+
};
|
|
158
|
+
const header = { alg, kid: jwk.kid, typ: "JWT" };
|
|
159
|
+
const encodedHeader = base64urlJson(header);
|
|
160
|
+
const encodedPayload = base64urlJson(payload);
|
|
161
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
162
|
+
const key = createPrivateKey({ key: jwk, format: "jwk" });
|
|
163
|
+
const signature = alg === "RS256" ? signBytes("RSA-SHA256", Buffer.from(signingInput), key) : signBytes("SHA256", Buffer.from(signingInput), {
|
|
164
|
+
key,
|
|
165
|
+
dsaEncoding: "ieee-p1363"
|
|
166
|
+
});
|
|
167
|
+
return `${signingInput}.${signature.toString("base64url")}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/lib/client.ts
|
|
171
|
+
var MISSING_HTTP_AUTH_CONTEXT_ERROR = "MCP HTTP requests require a validated OAuth tenant context before tool execution.";
|
|
172
|
+
function getClient() {
|
|
173
|
+
const oauthContext = tenantAuthContext();
|
|
174
|
+
if (oauthContext) {
|
|
175
|
+
const serviceToken = signMcpServiceToken(oauthContext);
|
|
176
|
+
const client = {
|
|
177
|
+
lastRequestId: null,
|
|
178
|
+
commerce: void 0,
|
|
179
|
+
collections: void 0,
|
|
180
|
+
community: void 0
|
|
181
|
+
};
|
|
182
|
+
const onRequestId = (id) => {
|
|
183
|
+
client.lastRequestId = id;
|
|
184
|
+
};
|
|
185
|
+
client.commerce = new ServerCommerceClient({
|
|
186
|
+
secretKey: serviceToken,
|
|
187
|
+
onRequestId
|
|
188
|
+
});
|
|
189
|
+
client.collections = new CollectionClient(
|
|
190
|
+
"",
|
|
191
|
+
serviceToken,
|
|
192
|
+
void 0,
|
|
193
|
+
void 0,
|
|
194
|
+
onRequestId
|
|
195
|
+
);
|
|
196
|
+
const community = new CommunityClient({ secretKey: serviceToken });
|
|
197
|
+
const moderation = new ModerationApi({ secretKey: serviceToken, onRequestId });
|
|
198
|
+
client.community = Object.assign(community, {
|
|
199
|
+
moderation: {
|
|
200
|
+
banCustomer: moderation.banCustomer.bind(moderation),
|
|
201
|
+
unbanCustomer: moderation.unbanCustomer.bind(moderation)
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
return client;
|
|
35
205
|
}
|
|
206
|
+
if (hasRequestContext()) throw new Error(MISSING_HTTP_AUTH_CONTEXT_ERROR);
|
|
207
|
+
const secretKey = process.env.SOFTWARE_SECRET_KEY;
|
|
208
|
+
const publishableKey = process.env.SOFTWARE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY;
|
|
36
209
|
if (!secretKey) {
|
|
37
210
|
throw new Error(
|
|
38
|
-
"Authentication required.
|
|
211
|
+
"Authentication required. Set SOFTWARE_SECRET_KEY for stdio transport."
|
|
39
212
|
);
|
|
40
213
|
}
|
|
41
214
|
if (!secretKey.startsWith("sk01_") && !secretKey.startsWith("pat01_")) {
|
|
42
|
-
throw new Error("Invalid
|
|
215
|
+
throw new Error("Invalid SOFTWARE_SECRET_KEY format. Expected sk01_ or pat01_ token.");
|
|
43
216
|
}
|
|
44
217
|
if (!publishableKey) {
|
|
45
218
|
throw new Error(
|
|
46
|
-
"publishableKey is required.
|
|
219
|
+
"publishableKey is required. Set SOFTWARE_PUBLISHABLE_KEY for stdio transport. It is used for rate limiting and monthly quota enforcement via the edge proxy."
|
|
47
220
|
);
|
|
48
221
|
}
|
|
49
222
|
return createServerClient({
|
|
@@ -1087,49 +1260,40 @@ import { COLLECTIONS as COLLECTIONS8 } from "@01.software/sdk";
|
|
|
1087
1260
|
import { createHash } from "crypto";
|
|
1088
1261
|
var BASE_URL = process.env.SOFTWARE_API_URL || "http://localhost:3000";
|
|
1089
1262
|
var TIMEOUT_MS = 5e3;
|
|
1263
|
+
var MISSING_HTTP_AUTH_CONTEXT_ERROR2 = "MCP HTTP requests require a validated OAuth tenant context before tool execution.";
|
|
1090
1264
|
function resolveAuthHeaderContext() {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
publishableKey: h?.["x-publishable-key"] ?? h?.["x-client-key"]
|
|
1265
|
+
const oauthContext = tenantAuthContext();
|
|
1266
|
+
if (oauthContext) {
|
|
1267
|
+
return {
|
|
1268
|
+
apiKey: signMcpServiceToken(oauthContext),
|
|
1269
|
+
mode: "oauth"
|
|
1097
1270
|
};
|
|
1098
|
-
} catch {
|
|
1099
1271
|
}
|
|
1272
|
+
if (hasRequestContext()) throw new Error(MISSING_HTTP_AUTH_CONTEXT_ERROR2);
|
|
1100
1273
|
return {
|
|
1101
|
-
apiKey:
|
|
1102
|
-
|
|
1274
|
+
apiKey: process.env.SOFTWARE_SECRET_KEY,
|
|
1275
|
+
mode: "stdio",
|
|
1276
|
+
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY
|
|
1103
1277
|
};
|
|
1104
1278
|
}
|
|
1105
1279
|
function resolveApiKey() {
|
|
1106
1280
|
const { apiKey } = resolveAuthHeaderContext();
|
|
1107
1281
|
if (!apiKey || typeof apiKey !== "string") {
|
|
1108
1282
|
throw new Error(
|
|
1109
|
-
"Authentication required.
|
|
1283
|
+
"Authentication required. Set SOFTWARE_SECRET_KEY for stdio transport."
|
|
1110
1284
|
);
|
|
1111
1285
|
}
|
|
1112
1286
|
return apiKey;
|
|
1113
1287
|
}
|
|
1114
|
-
function hashKey(apiKey) {
|
|
1115
|
-
return createHash("sha256").update(apiKey).digest("hex");
|
|
1116
|
-
}
|
|
1117
|
-
function resolveAuthCacheKey(apiKey) {
|
|
1118
|
-
const { publishableKey } = resolveAuthHeaderContext();
|
|
1119
|
-
return hashKey(
|
|
1120
|
-
JSON.stringify({
|
|
1121
|
-
apiKey,
|
|
1122
|
-
publishableKey: publishableKey ?? ""
|
|
1123
|
-
})
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
1288
|
function buildAuthHeaders(apiKey) {
|
|
1127
|
-
const { publishableKey } = resolveAuthHeaderContext();
|
|
1128
|
-
const
|
|
1289
|
+
const { mode, publishableKey } = resolveAuthHeaderContext();
|
|
1290
|
+
const headers = {
|
|
1129
1291
|
Authorization: `Bearer ${apiKey}`
|
|
1130
1292
|
};
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1293
|
+
if (mode === "stdio" && publishableKey) {
|
|
1294
|
+
headers["X-Publishable-Key"] = publishableKey;
|
|
1295
|
+
}
|
|
1296
|
+
return headers;
|
|
1133
1297
|
}
|
|
1134
1298
|
function extractErrorMessage(body) {
|
|
1135
1299
|
if (!body || typeof body !== "object") return void 0;
|
|
@@ -1223,37 +1387,18 @@ async function getCollectionSchemaTool({
|
|
|
1223
1387
|
import { z as z28 } from "zod";
|
|
1224
1388
|
|
|
1225
1389
|
// src/lib/tenant-context.ts
|
|
1226
|
-
var TENANT_CONTEXT_CACHE_TTL_MS = 6e4;
|
|
1227
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1228
1390
|
function getTenantContextPath(includeCounts) {
|
|
1229
1391
|
return includeCounts ? "/api/tenants/context?counts=true" : "/api/tenants/context";
|
|
1230
1392
|
}
|
|
1231
|
-
function getCachedTenantContext(cacheKey) {
|
|
1232
|
-
const cached = cache.get(cacheKey);
|
|
1233
|
-
if (!cached || cached.expiry <= Date.now()) return void 0;
|
|
1234
|
-
return cached.data;
|
|
1235
|
-
}
|
|
1236
1393
|
async function getTenantContext(includeCounts = false) {
|
|
1237
1394
|
const apiKey = resolveApiKey();
|
|
1238
|
-
const cacheKey = resolveAuthCacheKey(apiKey);
|
|
1239
|
-
if (!includeCounts) {
|
|
1240
|
-
const cached = getCachedTenantContext(cacheKey);
|
|
1241
|
-
if (cached) return cached;
|
|
1242
|
-
}
|
|
1243
1395
|
const data = await consoleGet(
|
|
1244
1396
|
getTenantContextPath(includeCounts),
|
|
1245
1397
|
apiKey
|
|
1246
1398
|
);
|
|
1247
|
-
if (!includeCounts) {
|
|
1248
|
-
cache.set(cacheKey, {
|
|
1249
|
-
data,
|
|
1250
|
-
expiry: Date.now() + TENANT_CONTEXT_CACHE_TTL_MS
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
1399
|
return data;
|
|
1254
1400
|
}
|
|
1255
1401
|
function invalidateTenantContextCache() {
|
|
1256
|
-
cache.clear();
|
|
1257
1402
|
}
|
|
1258
1403
|
|
|
1259
1404
|
// src/tools/get-tenant-context.ts
|
|
@@ -1339,18 +1484,12 @@ async function handler({ includeCounts }) {
|
|
|
1339
1484
|
import { z as z29 } from "zod";
|
|
1340
1485
|
|
|
1341
1486
|
// src/lib/field-config.ts
|
|
1342
|
-
var cache2 = /* @__PURE__ */ new Map();
|
|
1343
|
-
var CACHE_TTL = 6e4;
|
|
1344
1487
|
async function fetchFieldConfigs() {
|
|
1345
1488
|
const apiKey = resolveApiKey();
|
|
1346
|
-
const cacheKey = resolveAuthCacheKey(apiKey);
|
|
1347
|
-
const cached = cache2.get(cacheKey);
|
|
1348
|
-
if (cached && cached.expiry > Date.now()) return cached.data;
|
|
1349
1489
|
const data = await consoleGet(
|
|
1350
1490
|
"/api/field-configs/list",
|
|
1351
1491
|
apiKey
|
|
1352
1492
|
);
|
|
1353
|
-
cache2.set(cacheKey, { data, expiry: Date.now() + CACHE_TTL });
|
|
1354
1493
|
return data;
|
|
1355
1494
|
}
|
|
1356
1495
|
async function saveFieldConfig(body) {
|
|
@@ -1362,7 +1501,6 @@ async function saveFieldConfig(body) {
|
|
|
1362
1501
|
);
|
|
1363
1502
|
}
|
|
1364
1503
|
function invalidateFieldConfigCache() {
|
|
1365
|
-
cache2.clear();
|
|
1366
1504
|
}
|
|
1367
1505
|
|
|
1368
1506
|
// src/tools/list-configurable-fields.ts
|
|
@@ -1760,18 +1898,13 @@ const client = createClient({
|
|
|
1760
1898
|
})
|
|
1761
1899
|
|
|
1762
1900
|
// --- Register ---
|
|
1763
|
-
const { customer
|
|
1901
|
+
const { customer } = await client.customer.register({
|
|
1764
1902
|
name: 'Jane Doe',
|
|
1765
1903
|
email: 'jane@example.com',
|
|
1766
1904
|
password: 'securePassword123',
|
|
1767
1905
|
phone: '+821012345678', // optional
|
|
1768
1906
|
})
|
|
1769
1907
|
|
|
1770
|
-
if (verificationRequired) {
|
|
1771
|
-
// Tenant has requireEmailVerification enabled.
|
|
1772
|
-
// Token delivered via webhook \u2014 prompt user to check email.
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
1908
|
// --- Login ---
|
|
1776
1909
|
const { token, customer: loggedIn } = await client.customer.login({
|
|
1777
1910
|
email: 'jane@example.com',
|
|
@@ -1792,9 +1925,9 @@ await client.customer.forgotPassword('jane@example.com') // sends token via webh
|
|
|
1792
1925
|
await client.customer.resetPassword(token, 'newPassword123')`,
|
|
1793
1926
|
cautions: [
|
|
1794
1927
|
"customer.register/login/me are only available on Client (not ServerClient)",
|
|
1795
|
-
"
|
|
1928
|
+
"registration creates a local customer account; add app-level verification if your project requires it",
|
|
1796
1929
|
"updateProfile only accepts name, phone, and marketingConsent \u2014 not email or password",
|
|
1797
|
-
"forgotPassword sends the token
|
|
1930
|
+
"forgotPassword sends the token to configured tenant webhooks; your webhook handler owns email/SMS delivery"
|
|
1798
1931
|
],
|
|
1799
1932
|
relatedResources: ["docs://sdk/customer-auth", "docs://sdk/getting-started"],
|
|
1800
1933
|
relatedTools: []
|
|
@@ -2040,8 +2173,8 @@ var docIndex = [
|
|
|
2040
2173
|
// Customer Auth
|
|
2041
2174
|
{
|
|
2042
2175
|
title: "Customer Auth \u2014 Login and Register",
|
|
2043
|
-
keywords: ["customer", "login", "register", "auth", "authentication", "customer auth"
|
|
2044
|
-
summary: "client.customer.login({ email, password }) and register({ name, email, password }).
|
|
2176
|
+
keywords: ["customer", "login", "register", "auth", "authentication", "customer auth"],
|
|
2177
|
+
summary: "client.customer.login({ email, password }) and register({ name, email, password }).",
|
|
2045
2178
|
resourceUri: "docs://sdk/customer-auth"
|
|
2046
2179
|
},
|
|
2047
2180
|
{
|
|
@@ -2067,7 +2200,7 @@ var docIndex = [
|
|
|
2067
2200
|
{
|
|
2068
2201
|
title: "Webhooks",
|
|
2069
2202
|
keywords: ["webhook", "hmac", "signature", "WEBHOOK_SECRET", "server-to-server", "event"],
|
|
2070
|
-
summary: "Tenant webhooks deliver server-to-server events
|
|
2203
|
+
summary: "Tenant webhooks deliver server-to-server events such as password reset tokens. Signed with HMAC-SHA256 using PAYLOAD_SECRET.",
|
|
2071
2204
|
resourceUri: "docs://sdk/webhook"
|
|
2072
2205
|
},
|
|
2073
2206
|
// Order API
|
|
@@ -2159,13 +2292,13 @@ var schema33 = {
|
|
|
2159
2292
|
"server-client",
|
|
2160
2293
|
"customer-auth",
|
|
2161
2294
|
"mcp-connection",
|
|
2162
|
-
"
|
|
2295
|
+
"server-credentials",
|
|
2163
2296
|
"webhook-verification"
|
|
2164
2297
|
]).describe("Authentication scenario")
|
|
2165
2298
|
};
|
|
2166
2299
|
var metadata33 = {
|
|
2167
2300
|
name: "sdk-get-auth-setup",
|
|
2168
|
-
description: "Get the
|
|
2301
|
+
description: "Get the current authentication setup for a specific scenario. Returns env var names, code snippets, and security notes.",
|
|
2169
2302
|
annotations: {
|
|
2170
2303
|
title: "Get Auth Setup",
|
|
2171
2304
|
readOnlyHint: true,
|
|
@@ -2198,15 +2331,14 @@ const { data } = client.query.useQuery({ collection: 'products' })`,
|
|
|
2198
2331
|
|
|
2199
2332
|
const client = createServerClient({
|
|
2200
2333
|
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
|
|
2201
|
-
secretKey: process.env.SOFTWARE_SECRET_KEY!
|
|
2334
|
+
secretKey: process.env.SOFTWARE_SECRET_KEY!
|
|
2202
2335
|
})
|
|
2203
2336
|
|
|
2204
2337
|
// Full CRUD operations
|
|
2205
2338
|
const result = await client.collections.from('products').create({ title: 'New Product' })`,
|
|
2206
2339
|
notes: [
|
|
2207
|
-
"ServerClient has full CRUD access
|
|
2208
|
-
"
|
|
2209
|
-
"Browser-based CLI/init login flows may provision a user-scoped PAT (pat01_...) with a default tenant",
|
|
2340
|
+
"ServerClient has full CRUD access and must run only in trusted server code",
|
|
2341
|
+
"Store server credentials in environment variables and rotate them from the Console",
|
|
2210
2342
|
"Use in API routes, server actions, or backend services only",
|
|
2211
2343
|
"React Query hooks available for reads (useQuery, prefetchQuery, etc.) + mutations (useCreate, useUpdate, useRemove)"
|
|
2212
2344
|
]
|
|
@@ -2238,90 +2370,68 @@ client.customer.isAuthenticated()`,
|
|
|
2238
2370
|
notes: [
|
|
2239
2371
|
"Customer auth uses the browser Client (not ServerClient)",
|
|
2240
2372
|
"JWT tokens are managed automatically by the SDK",
|
|
2241
|
-
"
|
|
2373
|
+
"Registration creates a local customer account; add application-level verification if needed"
|
|
2242
2374
|
]
|
|
2243
2375
|
},
|
|
2244
2376
|
"mcp-connection": {
|
|
2245
2377
|
title: "MCP Server Connection",
|
|
2246
|
-
envVars: [
|
|
2378
|
+
envVars: [],
|
|
2247
2379
|
code: `# Claude Code
|
|
2248
|
-
claude mcp add --transport http
|
|
2249
|
-
--header "x-api-key: $SOFTWARE_SECRET_KEY" \\
|
|
2250
|
-
--header "x-publishable-key: $SOFTWARE_PUBLISHABLE_KEY" \\
|
|
2251
|
-
01software https://mcp.01.software/mcp
|
|
2380
|
+
claude mcp add --transport http 01software https://mcp.01.software/mcp
|
|
2252
2381
|
|
|
2253
|
-
# Codex
|
|
2382
|
+
# Codex .codex/config.toml
|
|
2254
2383
|
[mcp_servers.01software]
|
|
2255
2384
|
url = "https://mcp.01.software/mcp"
|
|
2256
2385
|
|
|
2257
|
-
|
|
2258
|
-
x-api-key = "SOFTWARE_SECRET_KEY"
|
|
2259
|
-
x-publishable-key = "SOFTWARE_PUBLISHABLE_KEY"
|
|
2260
|
-
|
|
2261
|
-
# Or use .mcp.json
|
|
2386
|
+
# Or use JSON clients that support OAuth discovery
|
|
2262
2387
|
{
|
|
2263
2388
|
"mcpServers": {
|
|
2264
2389
|
"01software": {
|
|
2265
2390
|
"type": "http",
|
|
2266
|
-
"url": "https://mcp.01.software/mcp"
|
|
2267
|
-
"headers": {
|
|
2268
|
-
"x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
|
|
2269
|
-
"x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
|
|
2270
|
-
}
|
|
2391
|
+
"url": "https://mcp.01.software/mcp"
|
|
2271
2392
|
}
|
|
2272
2393
|
}
|
|
2273
2394
|
}`,
|
|
2274
2395
|
notes: [
|
|
2275
|
-
"MCP
|
|
2276
|
-
"
|
|
2277
|
-
"
|
|
2278
|
-
"Use tenant API keys for shared service integrations; PATs are useful for user-scoped local workflows",
|
|
2279
|
-
"Never commit raw bearer tokens to repo-local MCP config; prefer environment interpolation, client prompts, OS secret managers, or ignored local files",
|
|
2280
|
-
"Avoid passing real tokens directly on shared-machine command lines because shell history and process listings can expose them",
|
|
2281
|
-
"stdio transport: use `npx @01.software/cli mcp` with SOFTWARE_PUBLISHABLE_KEY and SOFTWARE_SECRET_KEY env vars"
|
|
2396
|
+
"HTTP MCP uses OAuth discovery and Authorization Code + PKCE",
|
|
2397
|
+
"Clients that cannot complete OAuth discovery are unsupported until a smoke test proves compatibility",
|
|
2398
|
+
"stdio transport remains a local CLI path and is separate from HTTP MCP OAuth discovery"
|
|
2282
2399
|
]
|
|
2283
2400
|
},
|
|
2284
|
-
"
|
|
2285
|
-
title: "
|
|
2401
|
+
"server-credentials": {
|
|
2402
|
+
title: "Server Credential Management",
|
|
2286
2403
|
envVars: ["SOFTWARE_PUBLISHABLE_KEY", "SOFTWARE_SECRET_KEY"],
|
|
2287
|
-
code: `#
|
|
2288
|
-
#
|
|
2289
|
-
# The generated key has the format: sk01_{40hex}
|
|
2290
|
-
# Copy the publishable key from the same tenant.
|
|
2404
|
+
code: `# Server credentials are managed from the Console, not in code.
|
|
2405
|
+
# Copy both values from the same tenant.
|
|
2291
2406
|
|
|
2292
|
-
# Use them together for
|
|
2293
|
-
export SOFTWARE_PUBLISHABLE_KEY=
|
|
2407
|
+
# Use them together for CLI and server SDK calls.
|
|
2408
|
+
export SOFTWARE_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
2294
2409
|
export SOFTWARE_SECRET_KEY=sk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`,
|
|
2295
2410
|
notes: [
|
|
2296
|
-
"API keys are sk01_{40hex} opaque bearer tokens",
|
|
2297
2411
|
"The matching SOFTWARE_PUBLISHABLE_KEY is still required for tenant routing, rate limits, and quota enforcement",
|
|
2298
|
-
"
|
|
2299
|
-
"
|
|
2300
|
-
"Generate keys from Console > Settings > API Keys \u2014 never derive them in code"
|
|
2412
|
+
"Used for REST/SDK authentication in trusted server contexts",
|
|
2413
|
+
"Manage credentials from the Console and rotate them on exposure"
|
|
2301
2414
|
]
|
|
2302
2415
|
},
|
|
2303
2416
|
"webhook-verification": {
|
|
2304
2417
|
title: "Webhook Verification",
|
|
2305
2418
|
envVars: ["WEBHOOK_SECRET"],
|
|
2306
|
-
code: `import { handleWebhook } from '@01.software/sdk/webhook'
|
|
2419
|
+
code: `import { handleWebhook, createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'
|
|
2420
|
+
|
|
2421
|
+
const customerAuthHandler = createCustomerAuthWebhookHandler({
|
|
2422
|
+
passwordReset: sendPasswordResetEmail,
|
|
2423
|
+
})
|
|
2307
2424
|
|
|
2308
2425
|
export async function POST(request: Request) {
|
|
2309
|
-
return handleWebhook(request,
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
case 'verification':
|
|
2313
|
-
await sendVerificationEmail(event.data)
|
|
2314
|
-
break
|
|
2315
|
-
case 'password-reset':
|
|
2316
|
-
await sendPasswordResetEmail(event.data)
|
|
2317
|
-
break
|
|
2318
|
-
}
|
|
2319
|
-
}, { secret: process.env.WEBHOOK_SECRET! })
|
|
2426
|
+
return handleWebhook(request, customerAuthHandler, {
|
|
2427
|
+
secret: process.env.WEBHOOK_SECRET!,
|
|
2428
|
+
})
|
|
2320
2429
|
}`,
|
|
2321
2430
|
notes: [
|
|
2322
2431
|
"handleWebhook() takes (request, handler, options) \u2014 handler receives the parsed event",
|
|
2323
2432
|
"WEBHOOK_SECRET is set per-tenant in Console > Settings",
|
|
2324
|
-
"handleWebhook() verifies HMAC-SHA256 signature automatically before calling handler"
|
|
2433
|
+
"handleWebhook() verifies HMAC-SHA256 signature automatically before calling handler",
|
|
2434
|
+
"createCustomerAuthWebhookHandler() is optional; it just routes auth events to your own email/SMS delivery code"
|
|
2325
2435
|
]
|
|
2326
2436
|
}
|
|
2327
2437
|
};
|
|
@@ -2632,8 +2742,8 @@ await client.collections.from('products').remove('id')
|
|
|
2632
2742
|
const { totalDocs } = await client.collections.from('products').count()
|
|
2633
2743
|
|
|
2634
2744
|
// Metadata - generate Next.js Metadata from collection fields
|
|
2635
|
-
// Auto-maps per-collection fields (e.g.
|
|
2636
|
-
const
|
|
2745
|
+
// Auto-maps per-collection fields (e.g. articles: description\u2192description, thumbnail\u2192image)
|
|
2746
|
+
const articleMeta = await client.collections.from('articles').findMetadataById(id, { siteName: 'My Blog' })
|
|
2637
2747
|
const productMeta = await client.collections.from('products').findMetadata(
|
|
2638
2748
|
{ where: { slug: { equals: 'my-product' } } },
|
|
2639
2749
|
{ siteName: 'My Store' }
|
|
@@ -2959,7 +3069,7 @@ var schema38 = {
|
|
|
2959
3069
|
feature: z38.enum([
|
|
2960
3070
|
"ecommerce",
|
|
2961
3071
|
"customers",
|
|
2962
|
-
"
|
|
3072
|
+
"articles",
|
|
2963
3073
|
"documents",
|
|
2964
3074
|
"playlists",
|
|
2965
3075
|
"galleries",
|
|
@@ -3022,22 +3132,22 @@ customer-groups \u2014 Use \`create-collection\` with \`collection='customer-gro
|
|
|
3022
3132
|
|
|
3023
3133
|
### Config
|
|
3024
3134
|
|
|
3025
|
-
-
|
|
3135
|
+
- Customer registration creates a local account; add app-level verification if needed
|
|
3026
3136
|
- Customer auth uses custom JWT (separate from Payload auth)`,
|
|
3027
|
-
|
|
3137
|
+
articles: `## Articles Setup Guide
|
|
3028
3138
|
|
|
3029
3139
|
### Required Collections (count > 0)
|
|
3030
3140
|
|
|
3031
|
-
1. **
|
|
3141
|
+
1. **articles** \u2014 At least 1 article
|
|
3032
3142
|
- Minimum fields: \`{ title, slug }\`
|
|
3033
3143
|
|
|
3034
|
-
2. **
|
|
3144
|
+
2. **article-authors** \u2014 At least 1 author
|
|
3035
3145
|
- Minimum fields: \`{ title, slug }\`
|
|
3036
|
-
- Link authors to
|
|
3146
|
+
- Link authors to articles via the \`authors\` relationship field
|
|
3037
3147
|
|
|
3038
3148
|
### Optional Collections
|
|
3039
3149
|
|
|
3040
|
-
|
|
3150
|
+
article-categories, article-tags`,
|
|
3041
3151
|
documents: `## Documents Setup Guide
|
|
3042
3152
|
|
|
3043
3153
|
### Required Collections (count > 0)
|
|
@@ -3147,7 +3257,7 @@ form-submissions \u2014 Auto-created when forms are submitted by end users`,
|
|
|
3147
3257
|
|
|
3148
3258
|
### Required Collections (count > 0)
|
|
3149
3259
|
|
|
3150
|
-
1. **
|
|
3260
|
+
1. **posts** \u2014 At least 1 post
|
|
3151
3261
|
- Minimum fields: \`{ title, slug }\`
|
|
3152
3262
|
|
|
3153
3263
|
2. **reaction-types** \u2014 At least 1 reaction type defined
|
|
@@ -3159,7 +3269,7 @@ comments, reactions, bookmarks, reports, community-bans
|
|
|
3159
3269
|
|
|
3160
3270
|
### Optional Collections
|
|
3161
3271
|
|
|
3162
|
-
|
|
3272
|
+
post-categories`
|
|
3163
3273
|
};
|
|
3164
3274
|
function featureSetupGuide({ feature }) {
|
|
3165
3275
|
return `# Feature Setup Guide: ${feature}
|
|
@@ -3188,23 +3298,11 @@ function handler6() {
|
|
|
3188
3298
|
|
|
3189
3299
|
## Authentication
|
|
3190
3300
|
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
\`\`\`
|
|
3194
|
-
x-api-key: <sk01_... or pat01_...>
|
|
3195
|
-
x-publishable-key: <pk01_...>
|
|
3196
|
-
\`\`\`
|
|
3197
|
-
|
|
3198
|
-
\`x-client-key\` is accepted as a legacy alias for \`x-publishable-key\`.
|
|
3301
|
+
HTTP MCP uses OAuth discovery and Authorization Code + PKCE.
|
|
3199
3302
|
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
- \`pat01_{40hex}\` \u2014 personal access token for user-scoped local workflows
|
|
3204
|
-
|
|
3205
|
-
\`\`\`
|
|
3206
|
-
SOFTWARE_SECRET_KEY=sk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
3207
|
-
SOFTWARE_PUBLISHABLE_KEY=pk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
3303
|
+
\`\`\`toml
|
|
3304
|
+
[mcp_servers.01software]
|
|
3305
|
+
url = "https://mcp.01.software/mcp"
|
|
3208
3306
|
\`\`\`
|
|
3209
3307
|
|
|
3210
3308
|
## Available Tools (34)
|
|
@@ -3308,7 +3406,17 @@ function handler7() {
|
|
|
3308
3406
|
Carts: ["carts", "cart-items"],
|
|
3309
3407
|
Discounts: ["discounts"],
|
|
3310
3408
|
Documents: ["documents", "document-categories", "document-types"],
|
|
3311
|
-
|
|
3409
|
+
Articles: ["articles", "article-authors", "article-categories", "article-tags"],
|
|
3410
|
+
Community: [
|
|
3411
|
+
"posts",
|
|
3412
|
+
"comments",
|
|
3413
|
+
"reactions",
|
|
3414
|
+
"reaction-types",
|
|
3415
|
+
"bookmarks",
|
|
3416
|
+
"post-categories",
|
|
3417
|
+
"reports",
|
|
3418
|
+
"community-bans"
|
|
3419
|
+
],
|
|
3312
3420
|
Playlists: [
|
|
3313
3421
|
"playlists",
|
|
3314
3422
|
"tracks",
|
|
@@ -3859,7 +3967,7 @@ Customer authentication and profile management. Available on \`Client\` only (\`
|
|
|
3859
3967
|
### Authentication
|
|
3860
3968
|
\`\`\`typescript
|
|
3861
3969
|
// Register
|
|
3862
|
-
const { customer
|
|
3970
|
+
const { customer } = await client.customer.register({
|
|
3863
3971
|
name: 'John',
|
|
3864
3972
|
email: 'john@example.com',
|
|
3865
3973
|
password: 'password123',
|
|
@@ -3894,7 +4002,7 @@ const updated = await client.customer.updateProfile({
|
|
|
3894
4002
|
|
|
3895
4003
|
### Password
|
|
3896
4004
|
\`\`\`typescript
|
|
3897
|
-
// Forgot password (sends reset token
|
|
4005
|
+
// Forgot password (sends reset token to configured tenant webhooks)
|
|
3898
4006
|
await client.customer.forgotPassword(email)
|
|
3899
4007
|
|
|
3900
4008
|
// Reset password with token
|
|
@@ -3904,11 +4012,6 @@ await client.customer.resetPassword(token, newPassword)
|
|
|
3904
4012
|
await client.customer.changePassword(currentPassword, newPassword)
|
|
3905
4013
|
\`\`\`
|
|
3906
4014
|
|
|
3907
|
-
### Email Verification
|
|
3908
|
-
\`\`\`typescript
|
|
3909
|
-
await client.customer.verifyEmail(token)
|
|
3910
|
-
\`\`\`
|
|
3911
|
-
|
|
3912
4015
|
### Orders
|
|
3913
4016
|
\`\`\`typescript
|
|
3914
4017
|
const orders = await client.commerce.orders.listMine({
|
|
@@ -4087,7 +4190,7 @@ if (page1.hasNextPage) {
|
|
|
4087
4190
|
|
|
4088
4191
|
\`\`\`typescript
|
|
4089
4192
|
// Descending (newest first)
|
|
4090
|
-
const result = await client.collections.from('
|
|
4193
|
+
const result = await client.collections.from('articles').find({ sort: '-createdAt' })
|
|
4091
4194
|
|
|
4092
4195
|
// Ascending
|
|
4093
4196
|
const result2 = await client.collections.from('products').find({ sort: 'price' })
|
|
@@ -4382,19 +4485,19 @@ function handler13() {
|
|
|
4382
4485
|
|
|
4383
4486
|
Server-side operations are available via \`client.commerce\` on \`ServerClient\`. Use \`createServerClient\` with both \`publishableKey\` and \`secretKey\`.
|
|
4384
4487
|
|
|
4385
|
-
For backend services, prefer a tenant API key (\`sk01_...\`) in \`SOFTWARE_SECRET_KEY\`.
|
|
4386
|
-
Browser-based CLI/init login flows may instead provision a user-scoped PAT (\`pat01_...\`) with a default tenant.
|
|
4387
|
-
|
|
4388
4488
|
\`\`\`typescript
|
|
4389
4489
|
import { createServerClient } from '@01.software/sdk'
|
|
4390
4490
|
|
|
4391
4491
|
const client = createServerClient({
|
|
4392
4492
|
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
|
|
4393
|
-
secretKey: process.env.SOFTWARE_SECRET_KEY!,
|
|
4493
|
+
secretKey: process.env.SOFTWARE_SECRET_KEY!,
|
|
4394
4494
|
})
|
|
4395
4495
|
\`\`\`
|
|
4396
4496
|
|
|
4397
|
-
|
|
4497
|
+
Use server components, API routes, or server actions only. Never expose
|
|
4498
|
+
\`SOFTWARE_SECRET_KEY\` to browser code, client bundles, logs, or public
|
|
4499
|
+
repositories. If a secret key leaks, rotate it from the Console before deploying
|
|
4500
|
+
again.
|
|
4398
4501
|
|
|
4399
4502
|
## Order API
|
|
4400
4503
|
|
|
@@ -4663,11 +4766,9 @@ const result = await client.customer.register({
|
|
|
4663
4766
|
phone?: '+821012345678',
|
|
4664
4767
|
})
|
|
4665
4768
|
// result.customer - created customer object
|
|
4666
|
-
// result.token? - JWT token (set if email verification not required)
|
|
4667
|
-
// result.verificationRequired? - true if tenant requires email verification
|
|
4668
4769
|
\`\`\`
|
|
4669
4770
|
|
|
4670
|
-
|
|
4771
|
+
Registration creates a local customer account. Projects that need additional email verification should enforce it in application code.
|
|
4671
4772
|
|
|
4672
4773
|
### login()
|
|
4673
4774
|
Authenticate with email and password.
|
|
@@ -4721,12 +4822,12 @@ const updated = await client.customer.updateProfile({
|
|
|
4721
4822
|
## Password
|
|
4722
4823
|
|
|
4723
4824
|
### forgotPassword()
|
|
4724
|
-
Request a password reset. Sends reset token
|
|
4825
|
+
Request a password reset. Sends the reset token to configured tenant webhooks; your webhook handler owns delivery.
|
|
4725
4826
|
|
|
4726
4827
|
\`\`\`typescript
|
|
4727
4828
|
await client.customer.forgotPassword('john@example.com')
|
|
4728
4829
|
// Rate limited: 5 requests/min per tenant+email
|
|
4729
|
-
// Webhook receives: { resetPasswordToken,
|
|
4830
|
+
// Webhook receives: { resetPasswordToken, resetPasswordExpiresAt }
|
|
4730
4831
|
\`\`\`
|
|
4731
4832
|
|
|
4732
4833
|
### resetPassword()
|
|
@@ -4743,15 +4844,6 @@ Change password while authenticated (requires current password).
|
|
|
4743
4844
|
await client.customer.changePassword('currentPassword', 'newPassword123')
|
|
4744
4845
|
\`\`\`
|
|
4745
4846
|
|
|
4746
|
-
## Email Verification
|
|
4747
|
-
|
|
4748
|
-
### verifyEmail()
|
|
4749
|
-
Verify email address using the token received via webhook.
|
|
4750
|
-
|
|
4751
|
-
\`\`\`typescript
|
|
4752
|
-
await client.customer.verifyEmail('verification-token')
|
|
4753
|
-
\`\`\`
|
|
4754
|
-
|
|
4755
4847
|
## Orders
|
|
4756
4848
|
|
|
4757
4849
|
### listMine()
|
|
@@ -4797,12 +4889,7 @@ const client = createClient({
|
|
|
4797
4889
|
async function handleRegister(email: string, password: string, name: string) {
|
|
4798
4890
|
const result = await client.customer.register({ email, password, name })
|
|
4799
4891
|
|
|
4800
|
-
|
|
4801
|
-
// Redirect to "check your email" page
|
|
4802
|
-
return { status: 'verify-email' }
|
|
4803
|
-
}
|
|
4804
|
-
|
|
4805
|
-
// Token is automatically stored; customer is now logged in
|
|
4892
|
+
// Customer is created as a local account.
|
|
4806
4893
|
return { status: 'success', customer: result.customer }
|
|
4807
4894
|
}
|
|
4808
4895
|
|
|
@@ -4904,7 +4991,11 @@ await client.commerce.orders.checkout({ ... })
|
|
|
4904
4991
|
|
|
4905
4992
|
**Environment variables**:
|
|
4906
4993
|
- \`SOFTWARE_PUBLISHABLE_KEY\` \u2014 publishable key (no NEXT_PUBLIC prefix, server-only)
|
|
4907
|
-
- \`SOFTWARE_SECRET_KEY\` \u2014
|
|
4994
|
+
- \`SOFTWARE_SECRET_KEY\` \u2014 server credential
|
|
4995
|
+
|
|
4996
|
+
Never expose \`SOFTWARE_SECRET_KEY\` in browser code, client bundles, logs, or
|
|
4997
|
+
public repositories. If a secret key leaks, rotate it from the Console before
|
|
4998
|
+
deploying again.
|
|
4908
4999
|
|
|
4909
5000
|
## Decision Matrix
|
|
4910
5001
|
|
|
@@ -4970,9 +5061,10 @@ export function ProductList() {
|
|
|
4970
5061
|
|
|
4971
5062
|
## Security Rules
|
|
4972
5063
|
|
|
4973
|
-
-
|
|
5064
|
+
- Keep server credentials in server-only modules.
|
|
4974
5065
|
- Only \`NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY\` is safe to use in client components.
|
|
4975
|
-
-
|
|
5066
|
+
- Never import a module that reads \`SOFTWARE_SECRET_KEY\` from a client component.
|
|
5067
|
+
- Rotate any exposed secret key immediately from the Console.
|
|
4976
5068
|
|
|
4977
5069
|
> Ecommerce note: product card pricing lives on \`products.listing.*\`, but authoritative sellable pricing still lives on \`product-variants.price\`.`;
|
|
4978
5070
|
}
|
|
@@ -5137,27 +5229,23 @@ var metadata50 = {
|
|
|
5137
5229
|
function handler17() {
|
|
5138
5230
|
return `# Webhooks
|
|
5139
5231
|
|
|
5140
|
-
The platform dispatches HMAC-SHA256 signed webhook events to your registered URLs
|
|
5232
|
+
The platform dispatches HMAC-SHA256 signed webhook events to your registered URLs. Tenant developers own routing inside their webhook handler.
|
|
5141
5233
|
|
|
5142
5234
|
## Webhook Handling
|
|
5143
5235
|
|
|
5144
|
-
Use the SDK \`handleWebhook\` helper to verify signatures
|
|
5236
|
+
Use the SDK \`handleWebhook\` helper to verify signatures. For customer auth events, use \`createCustomerAuthWebhookHandler\` to wire delivery behavior in your app.
|
|
5145
5237
|
|
|
5146
5238
|
\`\`\`typescript
|
|
5147
|
-
import { handleWebhook } from '@01.software/sdk/webhook'
|
|
5239
|
+
import { handleWebhook, createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'
|
|
5240
|
+
|
|
5241
|
+
const handler = createCustomerAuthWebhookHandler({
|
|
5242
|
+
passwordReset: async (data) => {
|
|
5243
|
+
await sendPasswordResetEmail(data)
|
|
5244
|
+
},
|
|
5245
|
+
})
|
|
5148
5246
|
|
|
5149
5247
|
export async function POST(request: Request) {
|
|
5150
|
-
return handleWebhook(request,
|
|
5151
|
-
// event.collection, event.operation, event.data
|
|
5152
|
-
switch (event.operation) {
|
|
5153
|
-
case 'verification':
|
|
5154
|
-
await sendVerificationEmail(event.data)
|
|
5155
|
-
break
|
|
5156
|
-
case 'password-reset':
|
|
5157
|
-
await sendPasswordResetEmail(event.data)
|
|
5158
|
-
break
|
|
5159
|
-
}
|
|
5160
|
-
}, {
|
|
5248
|
+
return handleWebhook(request, handler, {
|
|
5161
5249
|
secret: process.env.WEBHOOK_SECRET!,
|
|
5162
5250
|
})
|
|
5163
5251
|
}
|
|
@@ -5169,19 +5257,17 @@ export async function POST(request: Request) {
|
|
|
5169
5257
|
|
|
5170
5258
|
\`\`\`typescript
|
|
5171
5259
|
// app/api/webhooks/route.ts
|
|
5172
|
-
import { handleWebhook } from '@01.software/sdk/webhook'
|
|
5260
|
+
import { handleWebhook, createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'
|
|
5261
|
+
|
|
5262
|
+
const customerAuthHandler = createCustomerAuthWebhookHandler({
|
|
5263
|
+
passwordReset: sendPasswordResetEmail,
|
|
5264
|
+
})
|
|
5173
5265
|
|
|
5174
5266
|
export async function POST(request: Request) {
|
|
5175
5267
|
return handleWebhook(request, async (event) => {
|
|
5176
5268
|
console.log('Webhook received:', event.collection, event.operation)
|
|
5177
5269
|
|
|
5178
|
-
|
|
5179
|
-
if (event.operation === 'verification') {
|
|
5180
|
-
await sendVerificationEmail(event.data)
|
|
5181
|
-
} else if (event.operation === 'password-reset') {
|
|
5182
|
-
await sendPasswordResetEmail(event.data)
|
|
5183
|
-
}
|
|
5184
|
-
}
|
|
5270
|
+
await customerAuthHandler(event)
|
|
5185
5271
|
}, {
|
|
5186
5272
|
secret: process.env.WEBHOOK_SECRET!,
|
|
5187
5273
|
})
|
|
@@ -5195,49 +5281,13 @@ All webhook events share this envelope:
|
|
|
5195
5281
|
\`\`\`typescript
|
|
5196
5282
|
{
|
|
5197
5283
|
collection: string, // e.g. 'customers'
|
|
5198
|
-
operation: string, // e.g. '
|
|
5284
|
+
operation: string, // e.g. 'password-reset'
|
|
5199
5285
|
data: object, // event-specific payload
|
|
5200
5286
|
}
|
|
5201
5287
|
\`\`\`
|
|
5202
5288
|
|
|
5203
5289
|
## Event Types
|
|
5204
5290
|
|
|
5205
|
-
### Customer Email Verification
|
|
5206
|
-
|
|
5207
|
-
Dispatched when a customer registers on a tenant with \`requireEmailVerification: true\`.
|
|
5208
|
-
|
|
5209
|
-
\`\`\`typescript
|
|
5210
|
-
{
|
|
5211
|
-
collection: 'customers',
|
|
5212
|
-
operation: 'verification',
|
|
5213
|
-
data: {
|
|
5214
|
-
customerId: string,
|
|
5215
|
-
email: string,
|
|
5216
|
-
name: string,
|
|
5217
|
-
verificationToken: string, // raw token to include in verification link
|
|
5218
|
-
}
|
|
5219
|
-
}
|
|
5220
|
-
\`\`\`
|
|
5221
|
-
|
|
5222
|
-
**Usage**: Send the \`verificationToken\` to the customer's email. The customer calls \`client.customer.verifyEmail(token)\` to complete verification.
|
|
5223
|
-
|
|
5224
|
-
\`\`\`typescript
|
|
5225
|
-
// Example: send verification email
|
|
5226
|
-
async function sendVerificationEmail(data: {
|
|
5227
|
-
customerId: string
|
|
5228
|
-
email: string
|
|
5229
|
-
name: string
|
|
5230
|
-
verificationToken: string
|
|
5231
|
-
}) {
|
|
5232
|
-
const verifyUrl = \`https://yourstore.com/verify-email?token=\${data.verificationToken}\`
|
|
5233
|
-
await emailService.send({
|
|
5234
|
-
to: data.email,
|
|
5235
|
-
subject: 'Verify your email',
|
|
5236
|
-
body: \`Click here to verify: \${verifyUrl}\`,
|
|
5237
|
-
})
|
|
5238
|
-
}
|
|
5239
|
-
\`\`\`
|
|
5240
|
-
|
|
5241
5291
|
### Customer Password Reset
|
|
5242
5292
|
|
|
5243
5293
|
Dispatched when a customer calls \`client.customer.forgotPassword(email)\`.
|
|
@@ -5251,7 +5301,7 @@ Dispatched when a customer calls \`client.customer.forgotPassword(email)\`.
|
|
|
5251
5301
|
email: string,
|
|
5252
5302
|
name: string,
|
|
5253
5303
|
resetPasswordToken: string, // raw token to include in reset link
|
|
5254
|
-
|
|
5304
|
+
resetPasswordExpiresAt: string, // ISO 8601 expiry (1 hour from dispatch)
|
|
5255
5305
|
}
|
|
5256
5306
|
}
|
|
5257
5307
|
\`\`\`
|
|
@@ -5264,13 +5314,13 @@ async function sendPasswordResetEmail(data: {
|
|
|
5264
5314
|
email: string
|
|
5265
5315
|
name: string
|
|
5266
5316
|
resetPasswordToken: string
|
|
5267
|
-
|
|
5317
|
+
resetPasswordExpiresAt: string
|
|
5268
5318
|
}) {
|
|
5269
5319
|
const resetUrl = \`https://yourstore.com/reset-password?token=\${data.resetPasswordToken}\`
|
|
5270
5320
|
await emailService.send({
|
|
5271
5321
|
to: data.email,
|
|
5272
5322
|
subject: 'Reset your password',
|
|
5273
|
-
body: \`Reset link (expires \${data.
|
|
5323
|
+
body: \`Reset link (expires \${data.resetPasswordExpiresAt}): \${resetUrl}\`,
|
|
5274
5324
|
})
|
|
5275
5325
|
}
|
|
5276
5326
|
\`\`\`
|
|
@@ -5333,37 +5383,40 @@ function registerStaticResource(server, uri, meta, handler19) {
|
|
|
5333
5383
|
})
|
|
5334
5384
|
);
|
|
5335
5385
|
}
|
|
5336
|
-
function createServer() {
|
|
5386
|
+
function createServer(options = {}) {
|
|
5387
|
+
const toolSurface = options.toolSurface ?? "full";
|
|
5337
5388
|
const server = new McpServer({
|
|
5338
5389
|
name: "01.software MCP Server",
|
|
5339
5390
|
version: "0.1.0"
|
|
5340
5391
|
});
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5392
|
+
if (toolSurface === "full") {
|
|
5393
|
+
registerTool(server, schema, metadata, queryCollection);
|
|
5394
|
+
registerTool(server, schema2, metadata2, getCollectionById);
|
|
5395
|
+
registerTool(server, schema3, metadata3, createCollection);
|
|
5396
|
+
registerTool(server, schema4, metadata4, updateCollection);
|
|
5397
|
+
registerTool(server, schema5, metadata5, deleteCollection);
|
|
5398
|
+
registerTool(server, schema6, metadata6, deleteManyCollection);
|
|
5399
|
+
registerTool(server, schema7, metadata7, updateManyCollection);
|
|
5400
|
+
registerTool(server, schema8, metadata8, getOrder);
|
|
5401
|
+
registerTool(server, schema9, metadata9, createOrder);
|
|
5402
|
+
registerTool(server, schema10, metadata10, updateOrder);
|
|
5403
|
+
registerTool(server, schema11, metadata11, checkout);
|
|
5404
|
+
registerTool(server, schema12, metadata12, createFulfillment);
|
|
5405
|
+
registerTool(server, schema13, metadata13, updateFulfillment);
|
|
5406
|
+
registerTool(server, schema14, metadata14, updateTransaction);
|
|
5407
|
+
registerTool(server, schema15, metadata15, createReturn);
|
|
5408
|
+
registerTool(server, schema16, metadata16, updateReturn);
|
|
5409
|
+
registerTool(server, schema17, metadata17, returnWithRefund);
|
|
5410
|
+
registerTool(server, schema18, metadata18, addCartItem);
|
|
5411
|
+
registerTool(server, schema19, metadata19, updateCartItem);
|
|
5412
|
+
registerTool(server, schema20, metadata20, removeCartItem);
|
|
5413
|
+
registerTool(server, schema21, metadata21, applyDiscount);
|
|
5414
|
+
registerTool(server, schema22, metadata22, removeDiscount);
|
|
5415
|
+
registerTool(server, schema23, metadata23, clearCart);
|
|
5416
|
+
registerTool(server, schema24, metadata24, validateDiscount);
|
|
5417
|
+
registerTool(server, schema25, metadata25, calculateShipping);
|
|
5418
|
+
registerTool(server, schema26, metadata26, stockCheck);
|
|
5419
|
+
}
|
|
5367
5420
|
registerTool(server, schema27, metadata27, getCollectionSchemaTool);
|
|
5368
5421
|
registerTool(server, schema28, metadata28, handler);
|
|
5369
5422
|
registerTool(server, schema29, metadata29, listConfigurableFields);
|
|
@@ -5392,21 +5445,198 @@ function createServer() {
|
|
|
5392
5445
|
}
|
|
5393
5446
|
|
|
5394
5447
|
// src/auth.ts
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5448
|
+
import { createPublicKey, verify as verifySignature } from "crypto";
|
|
5449
|
+
import {
|
|
5450
|
+
MCP_OAUTH_ISSUER as MCP_OAUTH_ISSUER2,
|
|
5451
|
+
MCP_RESOURCE_AUDIENCE,
|
|
5452
|
+
MCP_SCOPES,
|
|
5453
|
+
MCP_TENANT_CLAIM as MCP_TENANT_CLAIM2,
|
|
5454
|
+
MCP_TENANT_ROLE_CLAIM as MCP_TENANT_ROLE_CLAIM2
|
|
5455
|
+
} from "@01.software/auth-contracts";
|
|
5456
|
+
var ALLOWED_ALGORITHMS = /* @__PURE__ */ new Set(["RS256", "ES256"]);
|
|
5457
|
+
var DEFAULT_CLOCK_SKEW_SECONDS = 30;
|
|
5458
|
+
var DEFAULT_JWKS_URI = `${MCP_OAUTH_ISSUER2}/.well-known/jwks.json`;
|
|
5459
|
+
var MAX_ACCESS_TOKEN_LIFETIME_SECONDS = 300;
|
|
5460
|
+
function invalid(errorDescription) {
|
|
5461
|
+
return { valid: false, error: "invalid_token", errorDescription };
|
|
5462
|
+
}
|
|
5463
|
+
function isProduction() {
|
|
5464
|
+
return process.env.NODE_ENV === "production";
|
|
5465
|
+
}
|
|
5466
|
+
function insufficientScope(errorDescription) {
|
|
5467
|
+
return { valid: false, error: "insufficient_scope", errorDescription };
|
|
5468
|
+
}
|
|
5469
|
+
function decodeBase64UrlJson(value) {
|
|
5470
|
+
try {
|
|
5471
|
+
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
5472
|
+
} catch {
|
|
5473
|
+
return null;
|
|
5398
5474
|
}
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5475
|
+
}
|
|
5476
|
+
function parseScopes(payload) {
|
|
5477
|
+
const rawScopes = typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : Array.isArray(payload.scp) ? payload.scp : [];
|
|
5478
|
+
return rawScopes.filter(
|
|
5479
|
+
(scope) => scope === MCP_SCOPES.read || scope === MCP_SCOPES.write
|
|
5480
|
+
);
|
|
5481
|
+
}
|
|
5482
|
+
function audienceMatches(aud, expected) {
|
|
5483
|
+
if (typeof aud === "string") return aud === expected;
|
|
5484
|
+
return Array.isArray(aud) && aud.includes(expected);
|
|
5485
|
+
}
|
|
5486
|
+
function verifyJwtSignature(alg, jwk, signingInput, signature) {
|
|
5487
|
+
const key = createPublicKey({ key: jwk, format: "jwk" });
|
|
5488
|
+
const input = Buffer.from(signingInput);
|
|
5489
|
+
if (alg === "RS256") {
|
|
5490
|
+
return verifySignature("RSA-SHA256", input, key, signature);
|
|
5491
|
+
}
|
|
5492
|
+
if (alg === "ES256") {
|
|
5493
|
+
return verifySignature("SHA256", input, { key, dsaEncoding: "ieee-p1363" }, signature);
|
|
5494
|
+
}
|
|
5495
|
+
return false;
|
|
5496
|
+
}
|
|
5497
|
+
var cachedRemoteJwks = null;
|
|
5498
|
+
function jwksUriFor(options) {
|
|
5499
|
+
const raw = isProduction() ? DEFAULT_JWKS_URI : options.jwksUri ?? DEFAULT_JWKS_URI;
|
|
5500
|
+
let url;
|
|
5501
|
+
try {
|
|
5502
|
+
url = new URL(raw);
|
|
5503
|
+
} catch {
|
|
5504
|
+
return null;
|
|
5505
|
+
}
|
|
5506
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
|
|
5507
|
+
return null;
|
|
5508
|
+
}
|
|
5509
|
+
return url.toString();
|
|
5510
|
+
}
|
|
5511
|
+
async function remoteJwks(options = {}, forceRefresh = false) {
|
|
5512
|
+
const uri = jwksUriFor(options);
|
|
5513
|
+
if (!uri) return null;
|
|
5514
|
+
const now = Date.now();
|
|
5515
|
+
if (!forceRefresh && cachedRemoteJwks && cachedRemoteJwks.uri === uri && cachedRemoteJwks.expiresAt > now) {
|
|
5516
|
+
return cachedRemoteJwks.jwks;
|
|
5517
|
+
}
|
|
5518
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
5519
|
+
let response;
|
|
5520
|
+
try {
|
|
5521
|
+
response = await fetchImpl(uri, {
|
|
5522
|
+
headers: { Accept: "application/json" }
|
|
5523
|
+
});
|
|
5524
|
+
} catch {
|
|
5525
|
+
return null;
|
|
5526
|
+
}
|
|
5527
|
+
if (!response.ok) return null;
|
|
5528
|
+
const parsed = await response.json().catch(() => null);
|
|
5529
|
+
if (!parsed || !Array.isArray(parsed.keys)) return null;
|
|
5530
|
+
cachedRemoteJwks = {
|
|
5531
|
+
expiresAt: now + 3e5,
|
|
5532
|
+
jwks: parsed,
|
|
5533
|
+
uri
|
|
5534
|
+
};
|
|
5535
|
+
return parsed;
|
|
5536
|
+
}
|
|
5537
|
+
function validateAccessToken(token, options = {}) {
|
|
5538
|
+
const parts = token.split(".");
|
|
5539
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
5540
|
+
return invalid("Bearer token must be a compact JWT");
|
|
5541
|
+
}
|
|
5542
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
5543
|
+
const header = decodeBase64UrlJson(encodedHeader);
|
|
5544
|
+
const payload = decodeBase64UrlJson(encodedPayload);
|
|
5545
|
+
if (!header || !payload) return invalid("Bearer token contains invalid JSON");
|
|
5546
|
+
if (typeof header.alg !== "string" || !ALLOWED_ALGORITHMS.has(header.alg)) {
|
|
5547
|
+
return invalid("Bearer token uses an unsupported signing algorithm");
|
|
5548
|
+
}
|
|
5549
|
+
if (typeof header.kid !== "string" || header.kid.length === 0) {
|
|
5550
|
+
return invalid("Bearer token is missing kid");
|
|
5551
|
+
}
|
|
5552
|
+
const jwks = options.jwks;
|
|
5553
|
+
if (!jwks) return invalid("JWKS is not configured");
|
|
5554
|
+
const jwk = jwks.keys.find((key) => key.kid === header.kid);
|
|
5555
|
+
if (!jwk) return invalid("Bearer token kid is unknown");
|
|
5556
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
5557
|
+
const signature = Buffer.from(encodedSignature, "base64url");
|
|
5558
|
+
if (!verifyJwtSignature(header.alg, jwk, signingInput, signature)) {
|
|
5559
|
+
return invalid("Bearer token signature is invalid");
|
|
5560
|
+
}
|
|
5561
|
+
const issuer = options.issuer ?? MCP_OAUTH_ISSUER2;
|
|
5562
|
+
if (payload.iss !== issuer) return invalid("Bearer token issuer is invalid");
|
|
5563
|
+
const audience = options.audience ?? MCP_RESOURCE_AUDIENCE;
|
|
5564
|
+
if (!audienceMatches(payload.aud, audience)) {
|
|
5565
|
+
return invalid("Bearer token audience is invalid");
|
|
5566
|
+
}
|
|
5567
|
+
if (typeof payload.iat !== "number") return invalid("Bearer token is missing iat");
|
|
5568
|
+
if (typeof payload.exp !== "number") return invalid("Bearer token is missing exp");
|
|
5569
|
+
if (payload.exp <= payload.iat) {
|
|
5570
|
+
return invalid("Bearer token lifetime is invalid");
|
|
5571
|
+
}
|
|
5572
|
+
if (payload.exp - payload.iat > MAX_ACCESS_TOKEN_LIFETIME_SECONDS) {
|
|
5573
|
+
return invalid("Bearer token lifetime exceeds 300 seconds");
|
|
5574
|
+
}
|
|
5575
|
+
const nowSeconds = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
5576
|
+
const leeway = options.clockSkewSeconds ?? DEFAULT_CLOCK_SKEW_SECONDS;
|
|
5577
|
+
if (payload.iat > nowSeconds + leeway) {
|
|
5578
|
+
return invalid("Bearer token iat is too far in the future");
|
|
5579
|
+
}
|
|
5580
|
+
if (typeof payload.nbf === "number" && payload.nbf > nowSeconds + leeway) {
|
|
5581
|
+
return invalid("Bearer token is not yet valid");
|
|
5582
|
+
}
|
|
5583
|
+
if (payload.exp < nowSeconds - leeway) return invalid("Bearer token is expired");
|
|
5584
|
+
const tenantId = payload[MCP_TENANT_CLAIM2];
|
|
5585
|
+
if (typeof tenantId !== "string" || tenantId.length === 0) {
|
|
5586
|
+
return invalid("Bearer token tenant_id claim is invalid");
|
|
5404
5587
|
}
|
|
5405
|
-
|
|
5588
|
+
const tenantRole = payload[MCP_TENANT_ROLE_CLAIM2];
|
|
5589
|
+
if (tenantRole !== "tenant-admin" && tenantRole !== "tenant-editor" && tenantRole !== "tenant-viewer") {
|
|
5590
|
+
return invalid("Bearer token tenant_role claim is invalid");
|
|
5591
|
+
}
|
|
5592
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
5593
|
+
return invalid("Bearer token subject claim is invalid");
|
|
5594
|
+
}
|
|
5595
|
+
const scopes = parseScopes(payload);
|
|
5596
|
+
const requiredScopes = options.requiredScopes ?? [MCP_SCOPES.read];
|
|
5597
|
+
const missingScope = requiredScopes.find((scope) => !scopes.includes(scope));
|
|
5598
|
+
if (missingScope) return insufficientScope(`Bearer token is missing ${missingScope}`);
|
|
5599
|
+
return {
|
|
5600
|
+
valid: true,
|
|
5601
|
+
context: {
|
|
5602
|
+
tenantId,
|
|
5603
|
+
principalId: payload.sub,
|
|
5604
|
+
scopes,
|
|
5605
|
+
tenantRole,
|
|
5606
|
+
authMode: "oauth"
|
|
5607
|
+
}
|
|
5608
|
+
};
|
|
5609
|
+
}
|
|
5610
|
+
function shouldRefreshRemoteJwks(result) {
|
|
5611
|
+
return !result.valid && (result.errorDescription === "Bearer token kid is unknown" || result.errorDescription === "Bearer token signature is invalid");
|
|
5612
|
+
}
|
|
5613
|
+
async function validateBearerAuthorizationHeaderAsync(authorization, options) {
|
|
5614
|
+
if (!authorization) return invalid("Authorization Bearer token is required");
|
|
5615
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
5616
|
+
if (!match) return invalid("Authorization header must use Bearer");
|
|
5617
|
+
if (options?.jwks) {
|
|
5618
|
+
return validateAccessToken(match[1], options);
|
|
5619
|
+
}
|
|
5620
|
+
const jwks = await remoteJwks(options);
|
|
5621
|
+
const result = validateAccessToken(match[1], {
|
|
5622
|
+
...options,
|
|
5623
|
+
jwks: jwks ?? void 0
|
|
5624
|
+
});
|
|
5625
|
+
if (shouldRefreshRemoteJwks(result)) {
|
|
5626
|
+
const refreshedJwks = await remoteJwks(options, true);
|
|
5627
|
+
if (refreshedJwks) {
|
|
5628
|
+
return validateAccessToken(match[1], {
|
|
5629
|
+
...options,
|
|
5630
|
+
jwks: refreshedJwks
|
|
5631
|
+
});
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
return result;
|
|
5406
5635
|
}
|
|
5407
5636
|
|
|
5408
5637
|
// src/handler.ts
|
|
5409
5638
|
var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
5639
|
+
var MCP_ALLOWED_BROWSER_ORIGIN = "https://01.software";
|
|
5410
5640
|
var METHOD_NOT_ALLOWED = JSON.stringify({
|
|
5411
5641
|
jsonrpc: "2.0",
|
|
5412
5642
|
error: {
|
|
@@ -5416,16 +5646,16 @@ var METHOD_NOT_ALLOWED = JSON.stringify({
|
|
|
5416
5646
|
id: null
|
|
5417
5647
|
});
|
|
5418
5648
|
function setCors(res) {
|
|
5419
|
-
res.setHeader("Access-Control-Allow-Origin",
|
|
5649
|
+
res.setHeader("Access-Control-Allow-Origin", MCP_ALLOWED_BROWSER_ORIGIN);
|
|
5420
5650
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
5421
5651
|
res.setHeader(
|
|
5422
5652
|
"Access-Control-Allow-Headers",
|
|
5423
|
-
"Content-Type,
|
|
5653
|
+
"Authorization, Content-Type, mcp-session-id"
|
|
5424
5654
|
);
|
|
5425
5655
|
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id, Mcp-Protocol-Version");
|
|
5426
5656
|
}
|
|
5427
|
-
function getHeaderValue(
|
|
5428
|
-
const value =
|
|
5657
|
+
function getHeaderValue(headers, name) {
|
|
5658
|
+
const value = headers[name.toLowerCase()];
|
|
5429
5659
|
if (Array.isArray(value)) return value[0];
|
|
5430
5660
|
return value;
|
|
5431
5661
|
}
|
|
@@ -5450,143 +5680,69 @@ var HOME_PAGE = `01.software MCP Server
|
|
|
5450
5680
|
======================
|
|
5451
5681
|
|
|
5452
5682
|
MCP server for AI agents to interact with the 01.software API.
|
|
5453
|
-
|
|
5683
|
+
Connect over HTTP with OAuth, or use the local CLI stdio transport for full
|
|
5684
|
+
server-key operations.
|
|
5454
5685
|
|
|
5455
5686
|
|
|
5456
5687
|
Authentication
|
|
5457
5688
|
--------------
|
|
5458
5689
|
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
x-publishable-key publishable key for routing, rate limits, and quota enforcement
|
|
5462
|
-
|
|
5463
|
-
Use x-client-key as a legacy alias for x-publishable-key.
|
|
5464
|
-
|
|
5465
|
-
Accepted formats:
|
|
5466
|
-
sk01_{40hex} tenant API key (Console > Settings > API Keys)
|
|
5467
|
-
pat01_{40hex} personal access token (user-scoped local workflow)
|
|
5468
|
-
pk01_{...} publishable key
|
|
5469
|
-
|
|
5470
|
-
export SOFTWARE_PUBLISHABLE_KEY=pk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
5471
|
-
export SOFTWARE_SECRET_KEY=sk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
5472
|
-
|
|
5690
|
+
HTTP MCP uses OAuth discovery and Authorization Code + PKCE.
|
|
5691
|
+
Clients should connect to:
|
|
5473
5692
|
|
|
5474
5693
|
Connect
|
|
5475
5694
|
-------
|
|
5476
5695
|
|
|
5477
5696
|
Claude Code:
|
|
5478
5697
|
|
|
5479
|
-
claude mcp add --transport http
|
|
5480
|
-
--header "x-api-key: $SOFTWARE_SECRET_KEY" \\
|
|
5481
|
-
--header "x-publishable-key: $SOFTWARE_PUBLISHABLE_KEY" \\
|
|
5482
|
-
01software https://mcp.01.software/mcp
|
|
5698
|
+
claude mcp add --transport http 01software https://mcp.01.software/mcp
|
|
5483
5699
|
|
|
5484
|
-
Codex (.codex/config.toml
|
|
5700
|
+
Codex (.codex/config.toml):
|
|
5485
5701
|
|
|
5486
5702
|
[mcp_servers.01software]
|
|
5487
5703
|
url = "https://mcp.01.software/mcp"
|
|
5488
5704
|
|
|
5489
|
-
|
|
5490
|
-
x-api-key = "SOFTWARE_SECRET_KEY"
|
|
5491
|
-
x-publishable-key = "SOFTWARE_PUBLISHABLE_KEY"
|
|
5705
|
+
Cursor / Claude Desktop / other JSON clients:
|
|
5492
5706
|
|
|
5493
|
-
// or .mcp.json
|
|
5494
5707
|
{
|
|
5495
5708
|
"mcpServers": {
|
|
5496
5709
|
"01software": {
|
|
5497
5710
|
"type": "http",
|
|
5498
|
-
"url": "https://mcp.01.software/mcp"
|
|
5499
|
-
"headers": {
|
|
5500
|
-
"x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
|
|
5501
|
-
"x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
|
|
5502
|
-
}
|
|
5711
|
+
"url": "https://mcp.01.software/mcp"
|
|
5503
5712
|
}
|
|
5504
5713
|
}
|
|
5505
5714
|
}
|
|
5506
5715
|
|
|
5507
|
-
|
|
5716
|
+
Windsurf (~/.codeium/windsurf/mcp_config.json):
|
|
5508
5717
|
|
|
5509
5718
|
{
|
|
5510
5719
|
"mcpServers": {
|
|
5511
5720
|
"01software": {
|
|
5512
|
-
"
|
|
5513
|
-
"headers": {
|
|
5514
|
-
"x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
|
|
5515
|
-
"x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
|
|
5516
|
-
}
|
|
5721
|
+
"serverUrl": "https://mcp.01.software/mcp"
|
|
5517
5722
|
}
|
|
5518
5723
|
}
|
|
5519
5724
|
}
|
|
5520
5725
|
|
|
5521
|
-
|
|
5726
|
+
CLI (stdio):
|
|
5522
5727
|
|
|
5523
|
-
|
|
5524
|
-
"mcpServers": {
|
|
5525
|
-
"01software": {
|
|
5526
|
-
"serverUrl": "https://mcp.01.software/mcp",
|
|
5527
|
-
"headers": {
|
|
5528
|
-
"x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
|
|
5529
|
-
"x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
|
|
5530
|
-
}
|
|
5531
|
-
}
|
|
5532
|
-
}
|
|
5533
|
-
}
|
|
5728
|
+
npx @01.software/cli mcp
|
|
5534
5729
|
|
|
5535
|
-
VS Code (.vscode/mcp.json):
|
|
5536
5730
|
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
"01software": {
|
|
5540
|
-
"type": "http",
|
|
5541
|
-
"url": "https://mcp.01.software/mcp",
|
|
5542
|
-
"headers": {
|
|
5543
|
-
"x-api-key": "\${input:01software-api-key}",
|
|
5544
|
-
"x-publishable-key": "\${input:01software-publishable-key}"
|
|
5545
|
-
}
|
|
5546
|
-
}
|
|
5547
|
-
}
|
|
5548
|
-
}
|
|
5731
|
+
HTTP OAuth Tools
|
|
5732
|
+
----------------
|
|
5549
5733
|
|
|
5550
|
-
|
|
5734
|
+
Schema get-collection-schema
|
|
5735
|
+
Context get-tenant-context
|
|
5736
|
+
Field Config list-configurable-fields, update-field-config
|
|
5737
|
+
Guidance sdk-get-recipe, sdk-search-docs, sdk-get-auth-setup, sdk-get-collection-pattern
|
|
5551
5738
|
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
"01software": {
|
|
5555
|
-
"url": "https://mcp.01.software/mcp",
|
|
5556
|
-
"headers": {
|
|
5557
|
-
"x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
|
|
5558
|
-
"x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
|
|
5559
|
-
}
|
|
5560
|
-
}
|
|
5561
|
-
}
|
|
5562
|
-
}
|
|
5739
|
+
Full Tool Surface
|
|
5740
|
+
-----------------
|
|
5563
5741
|
|
|
5564
|
-
CLI
|
|
5742
|
+
The local CLI stdio transport exposes CRUD, commerce, cart, validation, product,
|
|
5743
|
+
schema, context, field-config, and guidance tools:
|
|
5565
5744
|
|
|
5566
5745
|
npx @01.software/cli mcp
|
|
5567
|
-
# Reads SOFTWARE_SECRET_KEY=sk01_... or pat01_...
|
|
5568
|
-
# Also reads SOFTWARE_PUBLISHABLE_KEY for CDN routing, rate limits, and quota enforcement
|
|
5569
|
-
|
|
5570
|
-
Security: never commit raw sk01_... or pat01_... tokens to repo-local MCP
|
|
5571
|
-
config files. Prefer client secret prompts, environment interpolation, OS secret
|
|
5572
|
-
managers, or ignored local files. Avoid passing real tokens directly on
|
|
5573
|
-
shared-machine command lines because shell history and process listings can
|
|
5574
|
-
expose them.
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
Tools (34)
|
|
5578
|
-
----------
|
|
5579
|
-
|
|
5580
|
-
CRUD query, get, create, update, delete, delete-many, update-many
|
|
5581
|
-
Orders create-order, checkout, get-order, update-order, create-fulfillment, update-fulfillment, update-transaction
|
|
5582
|
-
Returns create-return, update-return, return-with-refund
|
|
5583
|
-
Cart add-cart-item, update-cart-item, remove-cart-item, clear-cart, apply-discount, remove-discount
|
|
5584
|
-
Validation validate-discount, calculate-shipping
|
|
5585
|
-
Products stock-check
|
|
5586
|
-
Schema get-collection-schema
|
|
5587
|
-
Context get-tenant-context
|
|
5588
|
-
Field Config list-configurable-fields, update-field-config
|
|
5589
|
-
Guidance sdk-get-recipe, sdk-search-docs, sdk-get-auth-setup, sdk-get-collection-pattern
|
|
5590
5746
|
|
|
5591
5747
|
Prompts (4): sdk-usage-guide, collection-query-help, order-flow-guide, feature-setup-guide
|
|
5592
5748
|
Resources (12): config, collections-schema, getting-started, guides, api, query-builder, react-query, server-api, customer-auth, browser-vs-server, file-upload, webhook
|
|
@@ -5600,6 +5756,19 @@ SDK https://01.software/docs/sdk/client
|
|
|
5600
5756
|
API Reference https://01.software/docs/api/rest-api
|
|
5601
5757
|
Console https://console.01.software
|
|
5602
5758
|
`;
|
|
5759
|
+
var PROTECTED_RESOURCE_METADATA = JSON.stringify({
|
|
5760
|
+
resource: MCP_RESOURCE_AUDIENCE2,
|
|
5761
|
+
authorization_servers: [MCP_OAUTH_ISSUER3],
|
|
5762
|
+
scopes_supported: [MCP_SCOPES2.read, MCP_SCOPES2.write]
|
|
5763
|
+
});
|
|
5764
|
+
var SERVICE_JWKS_PATH = "/.well-known/service-jwks.json";
|
|
5765
|
+
function writeOAuthError(res, status, error, description) {
|
|
5766
|
+
res.writeHead(status, {
|
|
5767
|
+
"Content-Type": "application/json",
|
|
5768
|
+
"WWW-Authenticate": `Bearer resource_metadata="${MCP_PROTECTED_RESOURCE_METADATA_PATH}", error="${error}", error_description="${description}"`
|
|
5769
|
+
});
|
|
5770
|
+
res.end(JSON.stringify({ error, error_description: description }));
|
|
5771
|
+
}
|
|
5603
5772
|
async function handler18(req, res) {
|
|
5604
5773
|
setCors(res);
|
|
5605
5774
|
if (req.method === "OPTIONS") {
|
|
@@ -5608,6 +5777,30 @@ async function handler18(req, res) {
|
|
|
5608
5777
|
return;
|
|
5609
5778
|
}
|
|
5610
5779
|
if (req.method === "GET") {
|
|
5780
|
+
const pathname = new URL(req.url ?? "/", MCP_RESOURCE_AUDIENCE2).pathname;
|
|
5781
|
+
if (pathname === MCP_PROTECTED_RESOURCE_METADATA_PATH) {
|
|
5782
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
5783
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5784
|
+
res.end(PROTECTED_RESOURCE_METADATA);
|
|
5785
|
+
return;
|
|
5786
|
+
}
|
|
5787
|
+
if (pathname === SERVICE_JWKS_PATH) {
|
|
5788
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
5789
|
+
try {
|
|
5790
|
+
res.writeHead(200, {
|
|
5791
|
+
"Cache-Control": "public, max-age=60",
|
|
5792
|
+
"Content-Type": "application/json"
|
|
5793
|
+
});
|
|
5794
|
+
res.end(JSON.stringify(mcpServicePublicJwks()));
|
|
5795
|
+
} catch {
|
|
5796
|
+
res.writeHead(503, {
|
|
5797
|
+
"Cache-Control": "no-store",
|
|
5798
|
+
"Content-Type": "application/json"
|
|
5799
|
+
});
|
|
5800
|
+
res.end(JSON.stringify({ keys: [] }));
|
|
5801
|
+
}
|
|
5802
|
+
return;
|
|
5803
|
+
}
|
|
5611
5804
|
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
5612
5805
|
res.end(HOME_PAGE);
|
|
5613
5806
|
return;
|
|
@@ -5617,20 +5810,14 @@ async function handler18(req, res) {
|
|
|
5617
5810
|
res.end(METHOD_NOT_ALLOWED);
|
|
5618
5811
|
return;
|
|
5619
5812
|
}
|
|
5620
|
-
const
|
|
5621
|
-
|
|
5813
|
+
const auth = await validateBearerAuthorizationHeaderAsync(
|
|
5814
|
+
getHeaderValue(req.headers, "authorization")
|
|
5815
|
+
);
|
|
5622
5816
|
if (!auth.valid) {
|
|
5623
|
-
res
|
|
5624
|
-
res.end(JSON.stringify({ error: auth.error }));
|
|
5625
|
-
return;
|
|
5626
|
-
}
|
|
5627
|
-
const publishableKey = getHeaderValue(req.headers, "x-publishable-key") ?? getHeaderValue(req.headers, "x-client-key");
|
|
5628
|
-
if (!publishableKey) {
|
|
5629
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5630
|
-
res.end(JSON.stringify({ error: "x-publishable-key header is required" }));
|
|
5817
|
+
writeOAuthError(res, auth.error === "insufficient_scope" ? 403 : 401, auth.error, auth.errorDescription);
|
|
5631
5818
|
return;
|
|
5632
5819
|
}
|
|
5633
|
-
const server = createServer();
|
|
5820
|
+
const server = createServer({ toolSurface: "oauth" });
|
|
5634
5821
|
const transport = new StreamableHTTPServerTransport({
|
|
5635
5822
|
sessionIdGenerator: void 0
|
|
5636
5823
|
});
|
|
@@ -5645,12 +5832,12 @@ async function handler18(req, res) {
|
|
|
5645
5832
|
void close();
|
|
5646
5833
|
});
|
|
5647
5834
|
await server.connect(transport);
|
|
5648
|
-
const
|
|
5835
|
+
const headers = new Headers();
|
|
5649
5836
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
5650
|
-
if (typeof value === "string")
|
|
5651
|
-
else if (Array.isArray(value))
|
|
5837
|
+
if (typeof value === "string") headers.set(key, value);
|
|
5838
|
+
else if (Array.isArray(value)) headers.set(key, value.join(", "));
|
|
5652
5839
|
}
|
|
5653
|
-
await requestContext.run({ headers:
|
|
5840
|
+
await requestContext.run({ headers, auth: auth.context }, async () => {
|
|
5654
5841
|
try {
|
|
5655
5842
|
const body = req.body ?? JSON.parse(await readBody(req));
|
|
5656
5843
|
await transport.handleRequest(req, res, body);
|