sayren 0.6.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.
Files changed (2) hide show
  1. package/dist/index.mjs +187 -16
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -890,7 +890,8 @@ const cli_app_dev_error = ((inputs, options = {}) => {
890
890
  const ko_cli_app_dev_listening = (i) => {
891
891
  return `앱 로컬 실행 중 http://localhost:${i?.port} — 상점 ${i?.store} · 이벤트 ${i?.events}
892
892
  이벤트: npx sayren app trigger ORDER.PAID --order <orderId>
893
- HTTP: http://localhost:${i?.port}/_http/<경로>
893
+ HTTP: http://localhost:${i?.port}/_http/<경로> · 공개 HTTP: http://localhost:${i?.port}/hooks/<설치 id>/<경로>
894
+ 예약 작업: npx sayren app trigger --schedule <이름>
894
895
  설정·비밀 값: .env의 SAYREN_APP_SETTING_*·SAYREN_APP_SECRET_*
895
896
  종료는 Ctrl-C`;
896
897
  };
@@ -1036,7 +1037,7 @@ const cli_app_handle_mismatch = ((inputs, options = {}) => {
1036
1037
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1037
1038
  /** @typedef {{ events: NonNullable<unknown> }} Cli_App_Handler_MissingInputs */
1038
1039
  const ko_cli_app_handler_missing = (i) => {
1039
- return `매니페스트 events에 있지만 이벤트 함수가 없습니다: ${i?.events} (HANDLER_MISSING)`;
1040
+ return `매니페스트(events·schedules·http.public·flow.notifications·mcp.tools)에 있지만 코드에 함수가 없습니다: ${i?.events} (HANDLER_MISSING)`;
1040
1041
  };
1041
1042
  /**
1042
1043
 
@@ -1532,7 +1533,7 @@ const cli_app_trigger_failed = ((inputs, options = {}) => {
1532
1533
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1533
1534
  /** @typedef {{}} Cli_App_Trigger_Type_RequiredInputs */
1534
1535
  const ko_cli_app_trigger_type_required = () => {
1535
- return `이벤트 종류가 필요합니다. 예: npx sayren app trigger ORDER.PAID --order ord_…`;
1536
+ return `이벤트 종류가 필요합니다. 예: npx sayren app trigger ORDER.PAID --order ord_… (예약 작업은 --schedule <이름>)`;
1536
1537
  };
1537
1538
  /**
1538
1539
 
@@ -5249,11 +5250,15 @@ const ko_cli_usage = (i) => {
5249
5250
  --key <키> app: 앱 배포 키(developer.sayren.app에서 발행, SAYREN_APP_KEY). CI에서 deploy·release·status·logs·secret에 씁니다
5250
5251
  -m, --message <메모> app deploy: 버전 메모
5251
5252
  --name <이름> app init: 앱 이름(기본: handle)
5253
+ --private-store <storeId> app init: 이 상점 하나에만 설치되는 비공개 앱으로 만듭니다
5252
5254
  --port <포트> app dev·trigger: 로컬 포트(기본 4020)
5253
5255
  --since <기간> app logs: 최근 기간만(30m·1h·2d)
5254
5256
  --tail app logs: 새 기록을 계속 봅니다
5255
5257
  --data <json> app trigger: 이벤트 데이터
5256
5258
  --order <orderId> app trigger: 주문 id를 데이터에 넣습니다
5259
+ --schedule <이름> app trigger: 이벤트 대신 예약 작업을 실행합니다
5260
+ --flow <이름> app trigger: 이벤트 대신 흐름 알림을 보냅니다(--data는 알림 값)
5261
+ --tool <이름> app trigger: 이벤트 대신 에이전트 도구를 부릅니다(--data는 입력)
5257
5262
  --lang <로케일> 메시지 언어(기본: SAYREN_LANG → 시스템 로케일 → ko)
5258
5263
 
5259
5264
  종료 코드: 0 성공 · 1 실패 · 3 승인 대기(에이전트 토큰)`;
@@ -9177,7 +9182,7 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
9177
9182
 
9178
9183
  //#endregion
9179
9184
  //#region package.json
9180
- var version = "0.6.1";
9185
+ var version = "0.8.0";
9181
9186
 
9182
9187
  //#endregion
9183
9188
  //#region src/client.ts
@@ -9391,7 +9396,10 @@ async function loadBundle(dir, server) {
9391
9396
  app,
9392
9397
  meta: {
9393
9398
  events: [...meta.events],
9394
- http: [...meta.http]
9399
+ http: [...meta.http],
9400
+ schedules: [...meta.schedules ?? []],
9401
+ flows: [...meta.flows ?? []],
9402
+ tools: [...meta.tools ?? []]
9395
9403
  }
9396
9404
  };
9397
9405
  }
@@ -9432,14 +9440,39 @@ function collectListing(dir, manifest) {
9432
9440
  };
9433
9441
  }
9434
9442
  /** 로컬 검사 — 매니페스트, 번들, 이벤트 함수 존재, 비밀 값 형식, 크기. 서버 `VERIFY`·`BUNDLE`과 같은 규칙 */
9443
+ /**
9444
+ * 외부 런타임 앱(매뉴얼 「개발자 서버에서 실행」)이 올리는 서버 자리 — 코드는 개발자 서버에 있어 sayren 실행 환경에는 이것만 둔다(호출되지 않는다)
9445
+ */
9446
+ const EXTERNAL_STUB_SERVER = "export default { fetch() { return new Response(\"external runtime\", { status: 404 }); } };\n";
9447
+ const EMPTY_META = {
9448
+ events: [],
9449
+ http: [],
9450
+ schedules: [],
9451
+ flows: [],
9452
+ tools: []
9453
+ };
9435
9454
  async function verifyApp(dir) {
9436
9455
  const { raw, manifest } = loadManifest(dir);
9437
- const server = await bundleServer(dir);
9456
+ const external = manifest.runtime === "external";
9457
+ const server = external ? EXTERNAL_STUB_SERVER : await bundleServer(dir);
9438
9458
  if (APP_SECRET_PATTERNS.find((pattern) => pattern.test(server))) throw new CliError(cli_app_secret_in_source());
9439
9459
  const gzipBytes = gzipSync(server).byteLength;
9440
9460
  if (gzipBytes > APP_SERVER_BUNDLE_MAX_GZIP_BYTES) throw new CliError(cli_app_bundle_too_large({ size: formatMb(gzipBytes) }));
9441
- const { app, meta } = await loadBundle(dir, server);
9442
- const missing = manifest.events.filter((event) => !meta.events.includes(event));
9461
+ const { app, meta } = external ? {
9462
+ app: {
9463
+ fetch: async () => new Response("external runtime", { status: 404 }),
9464
+ __sayrenApp: EMPTY_META
9465
+ },
9466
+ meta: { ...EMPTY_META }
9467
+ } : await loadBundle(dir, server);
9468
+ const httpKeys = new Set(meta.http.map((key) => key.trim().replace(/\s+/g, " ")));
9469
+ const missing = external ? [] : [
9470
+ ...manifest.events.filter((event) => !meta.events.includes(event)),
9471
+ ...Object.keys(manifest.schedules).filter((name) => !meta.schedules.includes(name)),
9472
+ ...Object.keys(manifest.flow?.notifications ?? {}).filter((name) => !meta.flows.includes(name)),
9473
+ ...Object.keys(manifest.mcp?.tools ?? {}).filter((name) => !meta.tools.includes(name)),
9474
+ ...(manifest.http?.public ?? []).filter((key) => !httpKeys.has(key))
9475
+ ];
9443
9476
  if (missing.length) throw new CliError(cli_app_handler_missing({ events: missing.join(", ") }));
9444
9477
  const warnings = [];
9445
9478
  const listed = manifest.events;
@@ -9554,9 +9587,11 @@ async function appInit(handle, flags, io) {
9554
9587
  if (existsSync(join(dir, APP_MANIFEST_FILE))) throw new CliError(cli_app_dir_exists({ dir: relative(io.cwd, dir) || "." }));
9555
9588
  const name = flags.name?.trim() || handle;
9556
9589
  const auth = await developerAuth(flags, dir, io, { accountOnly: true });
9590
+ const privateStoreId = flags.privateStore?.trim();
9557
9591
  const app = await devApi(auth, io, "POST", "/apps", {
9558
9592
  handle,
9559
- name
9593
+ name,
9594
+ ...privateStoreId ? { privateStoreId } : {}
9560
9595
  });
9561
9596
  mkdirSync(join(dir, "src"), { recursive: true });
9562
9597
  for (const [path, content] of Object.entries(initFiles(handle, name))) {
@@ -9803,15 +9838,27 @@ function devEnvelope(input) {
9803
9838
  versionNumber: 0,
9804
9839
  storeId: input.token?.storeId ?? "store_dev",
9805
9840
  storeName: "(dev)",
9806
- apiBaseUrl: input.apiBaseUrl
9841
+ apiBaseUrl: input.apiBaseUrl,
9842
+ publicBaseUrl: `${input.publicOrigin}/hooks/${input.token?.installationId ?? "inst_dev"}`
9807
9843
  },
9808
9844
  token: input.token?.token ?? null,
9809
9845
  settings: input.settings,
9810
9846
  secrets: input.secrets,
9811
- ...input.kind === "http" ? { user: {
9847
+ ...input.kind === "http" && !input.public ? { user: {
9812
9848
  userId: "user_dev",
9813
9849
  role: "OWNER"
9814
9850
  } } : {},
9851
+ ...input.schedule ? { schedule: input.schedule } : {},
9852
+ ...input.tool ? { tool: input.tool } : {},
9853
+ ...input.flow ? { flow: {
9854
+ orderId: "ord_dev",
9855
+ orderItemIds: ["oi_dev"],
9856
+ flowId: "flow_dev",
9857
+ flowVersion: 1,
9858
+ state: "dev",
9859
+ params: {},
9860
+ ...input.flow
9861
+ } } : {},
9815
9862
  ...input.event ? { event: {
9816
9863
  id: `evt_dev_${devSeq}`,
9817
9864
  type: input.event.type,
@@ -9880,12 +9927,71 @@ async function appDev(flags, io) {
9880
9927
  await ensureToken();
9881
9928
  const local = () => localSettingsOf(manifest, readEnvFile(dir));
9882
9929
  const apiBaseUrl = `${state.apiOrigin}/v1`;
9930
+ const publicOrigin = `http://localhost:${port}`;
9883
9931
  const uiRoot = join(dir, UI_DIR);
9884
9932
  const handle = async (req, res) => {
9885
9933
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
9886
9934
  try {
9887
9935
  if (req.method === "POST" && url.pathname === "/_trigger") {
9888
9936
  const body = JSON.parse(await readBody(req) || "{}");
9937
+ if (body.tool) {
9938
+ const { settings: settings$1, secrets: secrets$1 } = local();
9939
+ const result$1 = await invokeLocal(app, devEnvelope({
9940
+ kind: "tool",
9941
+ tool: {
9942
+ name: body.tool,
9943
+ input: body.data ?? {}
9944
+ },
9945
+ publicOrigin,
9946
+ token: await ensureToken(),
9947
+ apiBaseUrl,
9948
+ appId: state.appId,
9949
+ settings: settings$1,
9950
+ secrets: secrets$1
9951
+ }));
9952
+ printResult(`[tool] ${body.tool}`, result$1, io);
9953
+ if (result$1.response?.body) io.out(` → ${result$1.response.body}`);
9954
+ res.writeHead(200, { "content-type": "application/json" });
9955
+ res.end(JSON.stringify(result$1));
9956
+ return;
9957
+ }
9958
+ if (body.flow) {
9959
+ const { settings: settings$1, secrets: secrets$1 } = local();
9960
+ const result$1 = await invokeLocal(app, devEnvelope({
9961
+ kind: "flow",
9962
+ flow: {
9963
+ ...body.data ?? {},
9964
+ name: body.flow
9965
+ },
9966
+ publicOrigin,
9967
+ token: await ensureToken(),
9968
+ apiBaseUrl,
9969
+ appId: state.appId,
9970
+ settings: settings$1,
9971
+ secrets: secrets$1
9972
+ }));
9973
+ printResult(`[flow] ${body.flow}`, result$1, io);
9974
+ res.writeHead(200, { "content-type": "application/json" });
9975
+ res.end(JSON.stringify(result$1));
9976
+ return;
9977
+ }
9978
+ if (body.schedule) {
9979
+ const { settings: settings$1, secrets: secrets$1 } = local();
9980
+ const result$1 = await invokeLocal(app, devEnvelope({
9981
+ kind: "schedule",
9982
+ schedule: body.schedule,
9983
+ publicOrigin,
9984
+ token: await ensureToken(),
9985
+ apiBaseUrl,
9986
+ appId: state.appId,
9987
+ settings: settings$1,
9988
+ secrets: secrets$1
9989
+ }));
9990
+ printResult(`[schedule] ${body.schedule}`, result$1, io);
9991
+ res.writeHead(200, { "content-type": "application/json" });
9992
+ res.end(JSON.stringify(result$1));
9993
+ return;
9994
+ }
9889
9995
  if (!body.type) {
9890
9996
  res.writeHead(400, { "content-type": "application/json" });
9891
9997
  res.end(JSON.stringify({ error: "TYPE_REQUIRED" }));
@@ -9894,6 +10000,7 @@ async function appDev(flags, io) {
9894
10000
  const { settings, secrets } = local();
9895
10001
  const result = await invokeLocal(app, devEnvelope({
9896
10002
  kind: "event",
10003
+ publicOrigin,
9897
10004
  token: await ensureToken(),
9898
10005
  apiBaseUrl,
9899
10006
  appId: state.appId,
@@ -9909,13 +10016,17 @@ async function appDev(flags, io) {
9909
10016
  res.end(JSON.stringify(result));
9910
10017
  return;
9911
10018
  }
9912
- if (url.pathname.startsWith("/_http/")) {
10019
+ const hook = /^\/hooks\/[^/]+(\/.*)$/.exec(url.pathname);
10020
+ if (url.pathname.startsWith("/_http/") || hook) {
10021
+ const route = hook ? hook[1] : url.pathname.slice(6);
9913
10022
  const headers = {};
9914
10023
  for (const [key, value] of Object.entries(req.headers)) if (typeof value === "string" && key !== "host") headers[key] = value;
9915
10024
  const method = (req.method ?? "GET").toUpperCase();
9916
10025
  const { settings, secrets } = local();
9917
10026
  const result = await invokeLocal(app, devEnvelope({
9918
10027
  kind: "http",
10028
+ public: Boolean(hook),
10029
+ publicOrigin,
9919
10030
  token: await ensureToken(),
9920
10031
  apiBaseUrl,
9921
10032
  appId: state.appId,
@@ -9923,12 +10034,12 @@ async function appDev(flags, io) {
9923
10034
  secrets,
9924
10035
  request: {
9925
10036
  method,
9926
- path: `${url.pathname.slice(6)}${url.search}`,
10037
+ path: `${route}${url.search}`,
9927
10038
  headers,
9928
10039
  body: method === "GET" ? null : await readBody(req)
9929
10040
  }
9930
10041
  }));
9931
- printResult(`[http] ${method} ${url.pathname}`, result, io);
10042
+ printResult(`[${hook ? "hook" : "http"}] ${method} ${route}`, result, io);
9932
10043
  if (!result.response) {
9933
10044
  res.writeHead(500, { "content-type": "application/json" });
9934
10045
  res.end(JSON.stringify({
@@ -9982,6 +10093,59 @@ async function appDev(flags, io) {
9982
10093
  return 0;
9983
10094
  }
9984
10095
  async function appTrigger(type, flags, io) {
10096
+ const port = Number(flags.port ?? io.env.SAYREN_APP_PORT ?? APP_DEV_PORT);
10097
+ if (flags.tool) {
10098
+ let toolInput = {};
10099
+ if (flags.data) try {
10100
+ toolInput = JSON.parse(flags.data);
10101
+ } catch {
10102
+ throw new CliError(cli_app_trigger_data_invalid());
10103
+ }
10104
+ const res$1 = await io.fetch(`http://127.0.0.1:${port}/_trigger`, {
10105
+ method: "POST",
10106
+ headers: { "content-type": "application/json" },
10107
+ body: JSON.stringify({
10108
+ tool: flags.tool,
10109
+ data: toolInput
10110
+ })
10111
+ });
10112
+ if (!res$1.ok) throw new CliError(cli_app_trigger_failed({ status: String(res$1.status) }));
10113
+ const result$1 = await res$1.json();
10114
+ printResult(`[tool] ${flags.tool}`, result$1, io);
10115
+ if (result$1.response?.body) io.out(` → ${result$1.response.body}`);
10116
+ return result$1.outcome === "OK" ? 0 : 1;
10117
+ }
10118
+ if (flags.flow) {
10119
+ let flowData = {};
10120
+ if (flags.data) try {
10121
+ flowData = JSON.parse(flags.data);
10122
+ } catch {
10123
+ throw new CliError(cli_app_trigger_data_invalid());
10124
+ }
10125
+ const res$1 = await io.fetch(`http://127.0.0.1:${port}/_trigger`, {
10126
+ method: "POST",
10127
+ headers: { "content-type": "application/json" },
10128
+ body: JSON.stringify({
10129
+ flow: flags.flow,
10130
+ data: flowData
10131
+ })
10132
+ });
10133
+ if (!res$1.ok) throw new CliError(cli_app_trigger_failed({ status: String(res$1.status) }));
10134
+ const result$1 = await res$1.json();
10135
+ printResult(`[flow] ${flags.flow}`, result$1, io);
10136
+ return result$1.outcome === "OK" ? 0 : 1;
10137
+ }
10138
+ if (flags.schedule) {
10139
+ const res$1 = await io.fetch(`http://127.0.0.1:${port}/_trigger`, {
10140
+ method: "POST",
10141
+ headers: { "content-type": "application/json" },
10142
+ body: JSON.stringify({ schedule: flags.schedule })
10143
+ });
10144
+ if (!res$1.ok) throw new CliError(cli_app_trigger_failed({ status: String(res$1.status) }));
10145
+ const result$1 = await res$1.json();
10146
+ printResult(`[schedule] ${flags.schedule}`, result$1, io);
10147
+ return result$1.outcome === "OK" ? 0 : 1;
10148
+ }
9985
10149
  if (!type) throw new CliError(cli_app_trigger_type_required());
9986
10150
  let data = {};
9987
10151
  if (flags.data) try {
@@ -9993,7 +10157,6 @@ async function appTrigger(type, flags, io) {
9993
10157
  ...data,
9994
10158
  orderId: flags.order
9995
10159
  };
9996
- const port = Number(flags.port ?? io.env.SAYREN_APP_PORT ?? APP_DEV_PORT);
9997
10160
  const res = await io.fetch(`http://127.0.0.1:${port}/_trigger`, {
9998
10161
  method: "POST",
9999
10162
  headers: { "content-type": "application/json" },
@@ -11905,7 +12068,11 @@ const VALUE_FLAGS = new Set([
11905
12068
  "port",
11906
12069
  "since",
11907
12070
  "data",
11908
- "order"
12071
+ "order",
12072
+ "schedule",
12073
+ "private-store",
12074
+ "flow",
12075
+ "tool"
11909
12076
  ]);
11910
12077
  const BOOL_FLAGS = new Set([
11911
12078
  "force",
@@ -11990,6 +12157,10 @@ async function main(argv, io) {
11990
12157
  tail: parsed.flags.tail === true,
11991
12158
  data: str(parsed.flags.data),
11992
12159
  order: str(parsed.flags.order),
12160
+ schedule: str(parsed.flags.schedule),
12161
+ privateStore: str(parsed.flags["private-store"]),
12162
+ flow: str(parsed.flags.flow),
12163
+ tool: str(parsed.flags.tool),
11993
12164
  yes: parsed.flags.yes === true
11994
12165
  }, io);
11995
12166
  case "init": return init({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sayren",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "sayren CLI — 호스팅 스토어프론트(init·pull·deploy)와 서드파티 앱(app init·dev·deploy·release)",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "esbuild": "^0.28.2",
17
- "@sayren/store-sdk": "^0.30.11"
17
+ "@sayren/store-sdk": "^0.30.13"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@biomejs/biome": "^2.5.14",