@stardeck-customer-apps/testing 0.27.0 → 0.28.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/SKILL.md +18 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +155 -2
- package/dist/index.mjs +155 -2
- package/dist/next/headers-shim.js +8 -0
- package/dist/next/headers-shim.mjs +8 -0
- package/dist/setup.js +100 -2
- package/dist/setup.mjs +100 -2
- package/package.json +6 -1
package/SKILL.md
CHANGED
|
@@ -624,6 +624,24 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
|
624
624
|
`expect(app.schedules.earliestWake("booking-janitor")!.getTime()).toBeLessThanOrEqual(dueAt.getTime())`.
|
|
625
625
|
`app.schedules.wakesFor(key)` lists them; `reset()` clears them. Run the job's handler
|
|
626
626
|
yourself to test what it does and the `nextRun()` it returns.
|
|
627
|
+
- `getControl` / `getControls` / `setControl` from `@stardeck-customer-apps/controls/server` — the
|
|
628
|
+
sync (`POST /api/deployments/:id/controls`) and write (`PUT .../controls/:key`) routes are
|
|
629
|
+
simulated, signature-checked like the others, and served from `app.controls`. Defaults are
|
|
630
|
+
permissive so existing tests keep passing: `flag.paymentsSetupStep: false`,
|
|
631
|
+
`plan.locations: { included: 3, max: null, extraMonthly: { amount: 990, currency: "THB" } }`,
|
|
632
|
+
`plan.multiEntity: true`, `payments.provider: "beam"`, `payments.methods: { card: true, promptPay: true }`.
|
|
633
|
+
Seed with `createTestApp({ controls: { "plan.multiEntity": false } })`. Then
|
|
634
|
+
`app.controls.set(key, value)` (the next `getControl` in the same process sees it: the
|
|
635
|
+
controls cache is dropped on every change and on `reset()`),
|
|
636
|
+
`app.controls.setUnavailable(key)` (that key reads `{ ok: false }`, like a platform outage),
|
|
637
|
+
`app.controls.lastUsage()` (the usage the app reported on its latest sync, for example
|
|
638
|
+
`{ locations: 4 }`), `app.controls.writes()` (each `setControl` the app made:
|
|
639
|
+
`{ key, value, userToken }`) and `app.controls.refuseNextWrite({ code, message, connectUrl? })`
|
|
640
|
+
(the next write fails once with that error, for example `PROVIDER_NOT_READY` with a connect
|
|
641
|
+
link). A write needs the person's session token in `X-Stardeck-User-Token`; without it the
|
|
642
|
+
simulator answers 401 `UNAUTHENTICATED`. Only `payments.provider` and `payments.methods` are
|
|
643
|
+
writable (anything else is 400 `NOT_WRITABLE`), and an invalid value is 400 `INVALID_VALUE`.
|
|
644
|
+
`app.controls.clear()` returns to the seeded values. Test the `ok: false` path of every gate.
|
|
627
645
|
|
|
628
646
|
### Receipt layout and printer capabilities
|
|
629
647
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _stardeck_customer_apps_payments_sdk from '@stardeck-customer-apps/payments-sdk';
|
|
2
2
|
import * as next_server from 'next/server';
|
|
3
|
+
import { ControlKey, ControlValue, ControlsUsage, ControlWriteErrorCode } from '@stardeck-customer-apps/controls';
|
|
3
4
|
import { PGlite } from '@electric-sql/pglite';
|
|
4
5
|
import { T as TestDataStoreInput } from './data-store-manifest-Zwgcv0DJ.mjs';
|
|
5
6
|
export { S as STARDECK_DATA_STORES_ENV, a as TestDataStoreManifestEntry, b as buildTestDataStoreManifest } from './data-store-manifest-Zwgcv0DJ.mjs';
|
|
@@ -745,7 +746,40 @@ interface TestSchedules {
|
|
|
745
746
|
earliestWake(scheduleKey: string): Date | undefined;
|
|
746
747
|
clear(): void;
|
|
747
748
|
}
|
|
749
|
+
interface CapturedControlWrite {
|
|
750
|
+
key: ControlKey;
|
|
751
|
+
value: unknown;
|
|
752
|
+
/** The person's session token the app sent in `X-Stardeck-User-Token`. */
|
|
753
|
+
userToken: string;
|
|
754
|
+
}
|
|
755
|
+
interface ControlRefusal {
|
|
756
|
+
code: ControlWriteErrorCode;
|
|
757
|
+
message: string;
|
|
758
|
+
connectUrl?: string;
|
|
759
|
+
}
|
|
760
|
+
/** The simulated app-controls sync and write routes (`@stardeck-customer-apps/controls`). */
|
|
761
|
+
interface TestControls {
|
|
762
|
+
/**
|
|
763
|
+
* Set a control's value (the app sees it on its next `getControl`, with no
|
|
764
|
+
* stale cache). Also makes a key that was forced unavailable available again.
|
|
765
|
+
*/
|
|
766
|
+
set<K extends ControlKey>(key: K, value: ControlValue<K>): void;
|
|
767
|
+
/** Make the sync report this control as `{ ok: false }`, like a platform outage for one key. */
|
|
768
|
+
setUnavailable(key: ControlKey): void;
|
|
769
|
+
/** The usage the app reported on its latest sync, or null if it reported none. */
|
|
770
|
+
lastUsage(): ControlsUsage | null;
|
|
771
|
+
/** Writes the app made through `setControl`, oldest first. */
|
|
772
|
+
writes(): CapturedControlWrite[];
|
|
773
|
+
/** The next write is refused with this error, then writes succeed again. */
|
|
774
|
+
refuseNextWrite(refusal: ControlRefusal): void;
|
|
775
|
+
/** Back to the values the test app was created with; clears writes, usage and refusals. */
|
|
776
|
+
clear(): void;
|
|
777
|
+
}
|
|
748
778
|
interface TestAppOptions {
|
|
779
|
+
/** Control values the simulated platform serves; unlisted keys keep the permissive defaults. */
|
|
780
|
+
controls?: Partial<{
|
|
781
|
+
[K in ControlKey]: ControlValue<K>;
|
|
782
|
+
}>;
|
|
749
783
|
/**
|
|
750
784
|
* The LIFF ID the platform forwards to the app for a LINE MINI App shop. It is
|
|
751
785
|
* what `GET /api/auth/me` returns as `liffId` and what makes `useLiff()` start
|
|
@@ -799,6 +833,8 @@ interface TestApp {
|
|
|
799
833
|
starlens: TestStarlens;
|
|
800
834
|
/** Wakes the app asked of its adaptive schedules (`wakeSchedule`). */
|
|
801
835
|
schedules: TestSchedules;
|
|
836
|
+
/** Platform-owned app controls: flags, plan limits, payment provider and methods. */
|
|
837
|
+
controls: TestControls;
|
|
802
838
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
803
839
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
804
840
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _stardeck_customer_apps_payments_sdk from '@stardeck-customer-apps/payments-sdk';
|
|
2
2
|
import * as next_server from 'next/server';
|
|
3
|
+
import { ControlKey, ControlValue, ControlsUsage, ControlWriteErrorCode } from '@stardeck-customer-apps/controls';
|
|
3
4
|
import { PGlite } from '@electric-sql/pglite';
|
|
4
5
|
import { T as TestDataStoreInput } from './data-store-manifest-Zwgcv0DJ.js';
|
|
5
6
|
export { S as STARDECK_DATA_STORES_ENV, a as TestDataStoreManifestEntry, b as buildTestDataStoreManifest } from './data-store-manifest-Zwgcv0DJ.js';
|
|
@@ -745,7 +746,40 @@ interface TestSchedules {
|
|
|
745
746
|
earliestWake(scheduleKey: string): Date | undefined;
|
|
746
747
|
clear(): void;
|
|
747
748
|
}
|
|
749
|
+
interface CapturedControlWrite {
|
|
750
|
+
key: ControlKey;
|
|
751
|
+
value: unknown;
|
|
752
|
+
/** The person's session token the app sent in `X-Stardeck-User-Token`. */
|
|
753
|
+
userToken: string;
|
|
754
|
+
}
|
|
755
|
+
interface ControlRefusal {
|
|
756
|
+
code: ControlWriteErrorCode;
|
|
757
|
+
message: string;
|
|
758
|
+
connectUrl?: string;
|
|
759
|
+
}
|
|
760
|
+
/** The simulated app-controls sync and write routes (`@stardeck-customer-apps/controls`). */
|
|
761
|
+
interface TestControls {
|
|
762
|
+
/**
|
|
763
|
+
* Set a control's value (the app sees it on its next `getControl`, with no
|
|
764
|
+
* stale cache). Also makes a key that was forced unavailable available again.
|
|
765
|
+
*/
|
|
766
|
+
set<K extends ControlKey>(key: K, value: ControlValue<K>): void;
|
|
767
|
+
/** Make the sync report this control as `{ ok: false }`, like a platform outage for one key. */
|
|
768
|
+
setUnavailable(key: ControlKey): void;
|
|
769
|
+
/** The usage the app reported on its latest sync, or null if it reported none. */
|
|
770
|
+
lastUsage(): ControlsUsage | null;
|
|
771
|
+
/** Writes the app made through `setControl`, oldest first. */
|
|
772
|
+
writes(): CapturedControlWrite[];
|
|
773
|
+
/** The next write is refused with this error, then writes succeed again. */
|
|
774
|
+
refuseNextWrite(refusal: ControlRefusal): void;
|
|
775
|
+
/** Back to the values the test app was created with; clears writes, usage and refusals. */
|
|
776
|
+
clear(): void;
|
|
777
|
+
}
|
|
748
778
|
interface TestAppOptions {
|
|
779
|
+
/** Control values the simulated platform serves; unlisted keys keep the permissive defaults. */
|
|
780
|
+
controls?: Partial<{
|
|
781
|
+
[K in ControlKey]: ControlValue<K>;
|
|
782
|
+
}>;
|
|
749
783
|
/**
|
|
750
784
|
* The LIFF ID the platform forwards to the app for a LINE MINI App shop. It is
|
|
751
785
|
* what `GET /api/auth/me` returns as `liffId` and what makes `useLiff()` start
|
|
@@ -799,6 +833,8 @@ interface TestApp {
|
|
|
799
833
|
starlens: TestStarlens;
|
|
800
834
|
/** Wakes the app asked of its adaptive schedules (`wakeSchedule`). */
|
|
801
835
|
schedules: TestSchedules;
|
|
836
|
+
/** Platform-owned app controls: flags, plan limits, payment provider and methods. */
|
|
837
|
+
controls: TestControls;
|
|
802
838
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
803
839
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
804
840
|
/**
|
package/dist/index.js
CHANGED
|
@@ -381,6 +381,14 @@ var state = globalSingleton("state", () => ({
|
|
|
381
381
|
starlensCalls: [],
|
|
382
382
|
starlensReplies: [],
|
|
383
383
|
starlensDefaultReply: null,
|
|
384
|
+
controls: {
|
|
385
|
+
seed: {},
|
|
386
|
+
values: {},
|
|
387
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
388
|
+
lastUsage: null,
|
|
389
|
+
writes: [],
|
|
390
|
+
refusal: null
|
|
391
|
+
},
|
|
384
392
|
allowNetwork: false
|
|
385
393
|
}));
|
|
386
394
|
function requireDb() {
|
|
@@ -4024,6 +4032,141 @@ function createEdge() {
|
|
|
4024
4032
|
};
|
|
4025
4033
|
}
|
|
4026
4034
|
|
|
4035
|
+
// src/simulator/controls.ts
|
|
4036
|
+
var CONTROLS_STATE_SYMBOL = /* @__PURE__ */ Symbol.for("stardeck.controls.state");
|
|
4037
|
+
var USER_TOKEN_HEADER = "x-stardeck-user-token";
|
|
4038
|
+
var DEFAULTS = {
|
|
4039
|
+
"flag.paymentsSetupStep": false,
|
|
4040
|
+
"plan.locations": { included: 3, max: null, extraMonthly: { amount: 990, currency: "THB" } },
|
|
4041
|
+
"plan.multiEntity": true,
|
|
4042
|
+
"payments.provider": "beam",
|
|
4043
|
+
"payments.methods": { card: true, promptPay: true }
|
|
4044
|
+
};
|
|
4045
|
+
var REFUSAL_STATUS = {
|
|
4046
|
+
NOT_WRITABLE: 400,
|
|
4047
|
+
INVALID_VALUE: 400,
|
|
4048
|
+
UNAUTHENTICATED: 401,
|
|
4049
|
+
FORBIDDEN: 403,
|
|
4050
|
+
WRITE_NOT_ALLOWED_HERE: 403,
|
|
4051
|
+
PROVIDER_NOT_READY: 409,
|
|
4052
|
+
UNAVAILABLE: 503
|
|
4053
|
+
};
|
|
4054
|
+
var controlsModule = null;
|
|
4055
|
+
function loadControls() {
|
|
4056
|
+
controlsModule ??= import("@stardeck-customer-apps/controls");
|
|
4057
|
+
return controlsModule;
|
|
4058
|
+
}
|
|
4059
|
+
function resetControlsClientCache() {
|
|
4060
|
+
const clientState = globalThis[CONTROLS_STATE_SYMBOL];
|
|
4061
|
+
clientState?.reset?.();
|
|
4062
|
+
}
|
|
4063
|
+
function initControls(seed) {
|
|
4064
|
+
state.controls.seed = { ...seed };
|
|
4065
|
+
restoreControls();
|
|
4066
|
+
}
|
|
4067
|
+
function restoreControls() {
|
|
4068
|
+
state.controls.values = { ...DEFAULTS, ...state.controls.seed };
|
|
4069
|
+
state.controls.unavailable = /* @__PURE__ */ new Set();
|
|
4070
|
+
state.controls.lastUsage = null;
|
|
4071
|
+
state.controls.writes = [];
|
|
4072
|
+
state.controls.refusal = null;
|
|
4073
|
+
resetControlsClientCache();
|
|
4074
|
+
}
|
|
4075
|
+
function clearControls() {
|
|
4076
|
+
restoreControls();
|
|
4077
|
+
}
|
|
4078
|
+
function currentControlsBody() {
|
|
4079
|
+
const controls = {};
|
|
4080
|
+
for (const [key, value] of Object.entries(state.controls.values)) {
|
|
4081
|
+
controls[key] = state.controls.unavailable.has(key) ? { ok: false, reason: "unavailable (simulated)" } : { ok: true, value };
|
|
4082
|
+
}
|
|
4083
|
+
return controls;
|
|
4084
|
+
}
|
|
4085
|
+
function refusalResponse(refusal) {
|
|
4086
|
+
return json({ error: refusal }, REFUSAL_STATUS[refusal.code]);
|
|
4087
|
+
}
|
|
4088
|
+
function handleSync(body) {
|
|
4089
|
+
const usage = body.usage;
|
|
4090
|
+
if (typeof usage === "object" && usage !== null) {
|
|
4091
|
+
const locations = usage.locations;
|
|
4092
|
+
state.controls.lastUsage = typeof locations === "number" && Number.isInteger(locations) && locations >= 0 ? { locations } : {};
|
|
4093
|
+
}
|
|
4094
|
+
return json({ controls: currentControlsBody() });
|
|
4095
|
+
}
|
|
4096
|
+
async function handleWrite(request, key) {
|
|
4097
|
+
const { CONTROLS, isWritableControl } = await loadControls();
|
|
4098
|
+
if (!isWritableControl(key)) {
|
|
4099
|
+
return refusalResponse({
|
|
4100
|
+
code: "NOT_WRITABLE",
|
|
4101
|
+
message: `${key} cannot be changed from the app`
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
const userToken = request.headers.get(USER_TOKEN_HEADER);
|
|
4105
|
+
if (!userToken) {
|
|
4106
|
+
return refusalResponse({
|
|
4107
|
+
code: "UNAUTHENTICATED",
|
|
4108
|
+
message: "A signed-in person is required (X-Stardeck-User-Token)"
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
const body = await readJsonBody(request);
|
|
4112
|
+
const parsed = CONTROLS[key].schema.safeParse(body.value);
|
|
4113
|
+
if (!parsed.success) {
|
|
4114
|
+
return refusalResponse({
|
|
4115
|
+
code: "INVALID_VALUE",
|
|
4116
|
+
message: parsed.error.issues[0]?.message ?? "Invalid value"
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
const refusal = state.controls.refusal;
|
|
4120
|
+
if (refusal) {
|
|
4121
|
+
state.controls.refusal = null;
|
|
4122
|
+
return refusalResponse(refusal);
|
|
4123
|
+
}
|
|
4124
|
+
state.controls.values[key] = parsed.data;
|
|
4125
|
+
state.controls.unavailable.delete(key);
|
|
4126
|
+
state.controls.writes.push({ key, value: parsed.data, userToken });
|
|
4127
|
+
return json({ controls: currentControlsBody() });
|
|
4128
|
+
}
|
|
4129
|
+
var CONTROLS_PATH = /^\/api\/deployments\/[^/]+\/controls(?:\/([^/]+))?$/;
|
|
4130
|
+
async function handleControlsRequest(request, url) {
|
|
4131
|
+
const match = url.pathname.match(CONTROLS_PATH);
|
|
4132
|
+
const key = match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
4133
|
+
if (!key && request.method === "POST") {
|
|
4134
|
+
return handleSync(await readJsonBody(request));
|
|
4135
|
+
}
|
|
4136
|
+
if (key && request.method === "PUT") {
|
|
4137
|
+
return handleWrite(request, key);
|
|
4138
|
+
}
|
|
4139
|
+
return json(
|
|
4140
|
+
{
|
|
4141
|
+
error: { code: "UNAVAILABLE", message: `No simulator for ${request.method} ${url.pathname}` }
|
|
4142
|
+
},
|
|
4143
|
+
404
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
function createControls() {
|
|
4147
|
+
return {
|
|
4148
|
+
set(key, value) {
|
|
4149
|
+
state.controls.values[key] = value;
|
|
4150
|
+
state.controls.unavailable.delete(key);
|
|
4151
|
+
resetControlsClientCache();
|
|
4152
|
+
},
|
|
4153
|
+
setUnavailable(key) {
|
|
4154
|
+
state.controls.unavailable.add(key);
|
|
4155
|
+
resetControlsClientCache();
|
|
4156
|
+
},
|
|
4157
|
+
lastUsage() {
|
|
4158
|
+
return state.controls.lastUsage;
|
|
4159
|
+
},
|
|
4160
|
+
writes() {
|
|
4161
|
+
return state.controls.writes;
|
|
4162
|
+
},
|
|
4163
|
+
refuseNextWrite(refusal) {
|
|
4164
|
+
state.controls.refusal = refusal;
|
|
4165
|
+
},
|
|
4166
|
+
clear: restoreControls
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4027
4170
|
// src/simulator/schedules.ts
|
|
4028
4171
|
async function handleScheduleWake(request, scheduleKey) {
|
|
4029
4172
|
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
@@ -4215,8 +4358,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
4215
4358
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
4216
4359
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
4217
4360
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4218
|
-
|
|
4361
|
+
const controlsMatch = url.pathname.match(CONTROLS_PATH2);
|
|
4362
|
+
return !!(controlsMatch || isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
4219
4363
|
}
|
|
4364
|
+
var CONTROLS_PATH2 = /^\/api\/deployments\/[^/]+\/controls(\/[^/]+)?$/;
|
|
4220
4365
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
4221
4366
|
function deploymentIdFromPath(url) {
|
|
4222
4367
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -4284,6 +4429,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
4284
4429
|
if (edgeMatch) {
|
|
4285
4430
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
4286
4431
|
}
|
|
4432
|
+
if (CONTROLS_PATH2.test(url.pathname)) {
|
|
4433
|
+
return handleControlsRequest(request, url);
|
|
4434
|
+
}
|
|
4287
4435
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4288
4436
|
if (scheduleWakeMatch) {
|
|
4289
4437
|
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
@@ -4312,7 +4460,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
4312
4460
|
}
|
|
4313
4461
|
}
|
|
4314
4462
|
return failure(
|
|
4315
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
4463
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, app controls sync/write, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
4316
4464
|
404
|
|
4317
4465
|
);
|
|
4318
4466
|
}
|
|
@@ -4461,6 +4609,7 @@ async function createTestApp(options = {}) {
|
|
|
4461
4609
|
const initialLiffId = options.liffId ?? null;
|
|
4462
4610
|
state.liffId = initialLiffId;
|
|
4463
4611
|
installFetchRouter();
|
|
4612
|
+
initControls(options.controls);
|
|
4464
4613
|
const schemaSql = loadSchemaSql(options) ?? "";
|
|
4465
4614
|
const db = await bootWithSchema(schemaSql);
|
|
4466
4615
|
state.db = db;
|
|
@@ -4492,6 +4641,7 @@ async function createTestApp(options = {}) {
|
|
|
4492
4641
|
const edge = createEdge();
|
|
4493
4642
|
const starlens = createStarlens();
|
|
4494
4643
|
const schedules = createSchedules();
|
|
4644
|
+
const controls = createControls();
|
|
4495
4645
|
const app = {
|
|
4496
4646
|
db,
|
|
4497
4647
|
inbox,
|
|
@@ -4502,6 +4652,7 @@ async function createTestApp(options = {}) {
|
|
|
4502
4652
|
edge,
|
|
4503
4653
|
starlens,
|
|
4504
4654
|
schedules,
|
|
4655
|
+
controls,
|
|
4505
4656
|
async query(sql, params = []) {
|
|
4506
4657
|
const result = await db.query(sql, params);
|
|
4507
4658
|
return result.rows;
|
|
@@ -4551,6 +4702,7 @@ async function createTestApp(options = {}) {
|
|
|
4551
4702
|
edge.clear();
|
|
4552
4703
|
starlens.clear();
|
|
4553
4704
|
schedules.clear();
|
|
4705
|
+
clearControls();
|
|
4554
4706
|
},
|
|
4555
4707
|
async close() {
|
|
4556
4708
|
state.db = null;
|
|
@@ -4567,6 +4719,7 @@ async function createTestApp(options = {}) {
|
|
|
4567
4719
|
edge.clear();
|
|
4568
4720
|
starlens.clear();
|
|
4569
4721
|
schedules.clear();
|
|
4722
|
+
clearControls();
|
|
4570
4723
|
uninstallFetchRouter();
|
|
4571
4724
|
await db.close();
|
|
4572
4725
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -342,6 +342,14 @@ var state = globalSingleton("state", () => ({
|
|
|
342
342
|
starlensCalls: [],
|
|
343
343
|
starlensReplies: [],
|
|
344
344
|
starlensDefaultReply: null,
|
|
345
|
+
controls: {
|
|
346
|
+
seed: {},
|
|
347
|
+
values: {},
|
|
348
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
349
|
+
lastUsage: null,
|
|
350
|
+
writes: [],
|
|
351
|
+
refusal: null
|
|
352
|
+
},
|
|
345
353
|
allowNetwork: false
|
|
346
354
|
}));
|
|
347
355
|
function requireDb() {
|
|
@@ -3985,6 +3993,141 @@ function createEdge() {
|
|
|
3985
3993
|
};
|
|
3986
3994
|
}
|
|
3987
3995
|
|
|
3996
|
+
// src/simulator/controls.ts
|
|
3997
|
+
var CONTROLS_STATE_SYMBOL = /* @__PURE__ */ Symbol.for("stardeck.controls.state");
|
|
3998
|
+
var USER_TOKEN_HEADER = "x-stardeck-user-token";
|
|
3999
|
+
var DEFAULTS = {
|
|
4000
|
+
"flag.paymentsSetupStep": false,
|
|
4001
|
+
"plan.locations": { included: 3, max: null, extraMonthly: { amount: 990, currency: "THB" } },
|
|
4002
|
+
"plan.multiEntity": true,
|
|
4003
|
+
"payments.provider": "beam",
|
|
4004
|
+
"payments.methods": { card: true, promptPay: true }
|
|
4005
|
+
};
|
|
4006
|
+
var REFUSAL_STATUS = {
|
|
4007
|
+
NOT_WRITABLE: 400,
|
|
4008
|
+
INVALID_VALUE: 400,
|
|
4009
|
+
UNAUTHENTICATED: 401,
|
|
4010
|
+
FORBIDDEN: 403,
|
|
4011
|
+
WRITE_NOT_ALLOWED_HERE: 403,
|
|
4012
|
+
PROVIDER_NOT_READY: 409,
|
|
4013
|
+
UNAVAILABLE: 503
|
|
4014
|
+
};
|
|
4015
|
+
var controlsModule = null;
|
|
4016
|
+
function loadControls() {
|
|
4017
|
+
controlsModule ??= import("@stardeck-customer-apps/controls");
|
|
4018
|
+
return controlsModule;
|
|
4019
|
+
}
|
|
4020
|
+
function resetControlsClientCache() {
|
|
4021
|
+
const clientState = globalThis[CONTROLS_STATE_SYMBOL];
|
|
4022
|
+
clientState?.reset?.();
|
|
4023
|
+
}
|
|
4024
|
+
function initControls(seed) {
|
|
4025
|
+
state.controls.seed = { ...seed };
|
|
4026
|
+
restoreControls();
|
|
4027
|
+
}
|
|
4028
|
+
function restoreControls() {
|
|
4029
|
+
state.controls.values = { ...DEFAULTS, ...state.controls.seed };
|
|
4030
|
+
state.controls.unavailable = /* @__PURE__ */ new Set();
|
|
4031
|
+
state.controls.lastUsage = null;
|
|
4032
|
+
state.controls.writes = [];
|
|
4033
|
+
state.controls.refusal = null;
|
|
4034
|
+
resetControlsClientCache();
|
|
4035
|
+
}
|
|
4036
|
+
function clearControls() {
|
|
4037
|
+
restoreControls();
|
|
4038
|
+
}
|
|
4039
|
+
function currentControlsBody() {
|
|
4040
|
+
const controls = {};
|
|
4041
|
+
for (const [key, value] of Object.entries(state.controls.values)) {
|
|
4042
|
+
controls[key] = state.controls.unavailable.has(key) ? { ok: false, reason: "unavailable (simulated)" } : { ok: true, value };
|
|
4043
|
+
}
|
|
4044
|
+
return controls;
|
|
4045
|
+
}
|
|
4046
|
+
function refusalResponse(refusal) {
|
|
4047
|
+
return json({ error: refusal }, REFUSAL_STATUS[refusal.code]);
|
|
4048
|
+
}
|
|
4049
|
+
function handleSync(body) {
|
|
4050
|
+
const usage = body.usage;
|
|
4051
|
+
if (typeof usage === "object" && usage !== null) {
|
|
4052
|
+
const locations = usage.locations;
|
|
4053
|
+
state.controls.lastUsage = typeof locations === "number" && Number.isInteger(locations) && locations >= 0 ? { locations } : {};
|
|
4054
|
+
}
|
|
4055
|
+
return json({ controls: currentControlsBody() });
|
|
4056
|
+
}
|
|
4057
|
+
async function handleWrite(request, key) {
|
|
4058
|
+
const { CONTROLS, isWritableControl } = await loadControls();
|
|
4059
|
+
if (!isWritableControl(key)) {
|
|
4060
|
+
return refusalResponse({
|
|
4061
|
+
code: "NOT_WRITABLE",
|
|
4062
|
+
message: `${key} cannot be changed from the app`
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
const userToken = request.headers.get(USER_TOKEN_HEADER);
|
|
4066
|
+
if (!userToken) {
|
|
4067
|
+
return refusalResponse({
|
|
4068
|
+
code: "UNAUTHENTICATED",
|
|
4069
|
+
message: "A signed-in person is required (X-Stardeck-User-Token)"
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
const body = await readJsonBody(request);
|
|
4073
|
+
const parsed = CONTROLS[key].schema.safeParse(body.value);
|
|
4074
|
+
if (!parsed.success) {
|
|
4075
|
+
return refusalResponse({
|
|
4076
|
+
code: "INVALID_VALUE",
|
|
4077
|
+
message: parsed.error.issues[0]?.message ?? "Invalid value"
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
const refusal = state.controls.refusal;
|
|
4081
|
+
if (refusal) {
|
|
4082
|
+
state.controls.refusal = null;
|
|
4083
|
+
return refusalResponse(refusal);
|
|
4084
|
+
}
|
|
4085
|
+
state.controls.values[key] = parsed.data;
|
|
4086
|
+
state.controls.unavailable.delete(key);
|
|
4087
|
+
state.controls.writes.push({ key, value: parsed.data, userToken });
|
|
4088
|
+
return json({ controls: currentControlsBody() });
|
|
4089
|
+
}
|
|
4090
|
+
var CONTROLS_PATH = /^\/api\/deployments\/[^/]+\/controls(?:\/([^/]+))?$/;
|
|
4091
|
+
async function handleControlsRequest(request, url) {
|
|
4092
|
+
const match = url.pathname.match(CONTROLS_PATH);
|
|
4093
|
+
const key = match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
4094
|
+
if (!key && request.method === "POST") {
|
|
4095
|
+
return handleSync(await readJsonBody(request));
|
|
4096
|
+
}
|
|
4097
|
+
if (key && request.method === "PUT") {
|
|
4098
|
+
return handleWrite(request, key);
|
|
4099
|
+
}
|
|
4100
|
+
return json(
|
|
4101
|
+
{
|
|
4102
|
+
error: { code: "UNAVAILABLE", message: `No simulator for ${request.method} ${url.pathname}` }
|
|
4103
|
+
},
|
|
4104
|
+
404
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
function createControls() {
|
|
4108
|
+
return {
|
|
4109
|
+
set(key, value) {
|
|
4110
|
+
state.controls.values[key] = value;
|
|
4111
|
+
state.controls.unavailable.delete(key);
|
|
4112
|
+
resetControlsClientCache();
|
|
4113
|
+
},
|
|
4114
|
+
setUnavailable(key) {
|
|
4115
|
+
state.controls.unavailable.add(key);
|
|
4116
|
+
resetControlsClientCache();
|
|
4117
|
+
},
|
|
4118
|
+
lastUsage() {
|
|
4119
|
+
return state.controls.lastUsage;
|
|
4120
|
+
},
|
|
4121
|
+
writes() {
|
|
4122
|
+
return state.controls.writes;
|
|
4123
|
+
},
|
|
4124
|
+
refuseNextWrite(refusal) {
|
|
4125
|
+
state.controls.refusal = refusal;
|
|
4126
|
+
},
|
|
4127
|
+
clear: restoreControls
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
|
|
3988
4131
|
// src/simulator/schedules.ts
|
|
3989
4132
|
async function handleScheduleWake(request, scheduleKey) {
|
|
3990
4133
|
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
@@ -4176,8 +4319,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
4176
4319
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
4177
4320
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
4178
4321
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4179
|
-
|
|
4322
|
+
const controlsMatch = url.pathname.match(CONTROLS_PATH2);
|
|
4323
|
+
return !!(controlsMatch || isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
4180
4324
|
}
|
|
4325
|
+
var CONTROLS_PATH2 = /^\/api\/deployments\/[^/]+\/controls(\/[^/]+)?$/;
|
|
4181
4326
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
4182
4327
|
function deploymentIdFromPath(url) {
|
|
4183
4328
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -4245,6 +4390,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
4245
4390
|
if (edgeMatch) {
|
|
4246
4391
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
4247
4392
|
}
|
|
4393
|
+
if (CONTROLS_PATH2.test(url.pathname)) {
|
|
4394
|
+
return handleControlsRequest(request, url);
|
|
4395
|
+
}
|
|
4248
4396
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4249
4397
|
if (scheduleWakeMatch) {
|
|
4250
4398
|
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
@@ -4273,7 +4421,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
4273
4421
|
}
|
|
4274
4422
|
}
|
|
4275
4423
|
return failure(
|
|
4276
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
4424
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, app controls sync/write, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
4277
4425
|
404
|
|
4278
4426
|
);
|
|
4279
4427
|
}
|
|
@@ -4422,6 +4570,7 @@ async function createTestApp(options = {}) {
|
|
|
4422
4570
|
const initialLiffId = options.liffId ?? null;
|
|
4423
4571
|
state.liffId = initialLiffId;
|
|
4424
4572
|
installFetchRouter();
|
|
4573
|
+
initControls(options.controls);
|
|
4425
4574
|
const schemaSql = loadSchemaSql(options) ?? "";
|
|
4426
4575
|
const db = await bootWithSchema(schemaSql);
|
|
4427
4576
|
state.db = db;
|
|
@@ -4453,6 +4602,7 @@ async function createTestApp(options = {}) {
|
|
|
4453
4602
|
const edge = createEdge();
|
|
4454
4603
|
const starlens = createStarlens();
|
|
4455
4604
|
const schedules = createSchedules();
|
|
4605
|
+
const controls = createControls();
|
|
4456
4606
|
const app = {
|
|
4457
4607
|
db,
|
|
4458
4608
|
inbox,
|
|
@@ -4463,6 +4613,7 @@ async function createTestApp(options = {}) {
|
|
|
4463
4613
|
edge,
|
|
4464
4614
|
starlens,
|
|
4465
4615
|
schedules,
|
|
4616
|
+
controls,
|
|
4466
4617
|
async query(sql, params = []) {
|
|
4467
4618
|
const result = await db.query(sql, params);
|
|
4468
4619
|
return result.rows;
|
|
@@ -4512,6 +4663,7 @@ async function createTestApp(options = {}) {
|
|
|
4512
4663
|
edge.clear();
|
|
4513
4664
|
starlens.clear();
|
|
4514
4665
|
schedules.clear();
|
|
4666
|
+
clearControls();
|
|
4515
4667
|
},
|
|
4516
4668
|
async close() {
|
|
4517
4669
|
state.db = null;
|
|
@@ -4528,6 +4680,7 @@ async function createTestApp(options = {}) {
|
|
|
4528
4680
|
edge.clear();
|
|
4529
4681
|
starlens.clear();
|
|
4530
4682
|
schedules.clear();
|
|
4683
|
+
clearControls();
|
|
4531
4684
|
uninstallFetchRouter();
|
|
4532
4685
|
await db.close();
|
|
4533
4686
|
}
|
|
@@ -103,6 +103,14 @@ var state = globalSingleton("state", () => ({
|
|
|
103
103
|
starlensCalls: [],
|
|
104
104
|
starlensReplies: [],
|
|
105
105
|
starlensDefaultReply: null,
|
|
106
|
+
controls: {
|
|
107
|
+
seed: {},
|
|
108
|
+
values: {},
|
|
109
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
110
|
+
lastUsage: null,
|
|
111
|
+
writes: [],
|
|
112
|
+
refusal: null
|
|
113
|
+
},
|
|
106
114
|
allowNetwork: false
|
|
107
115
|
}));
|
|
108
116
|
|
|
@@ -76,6 +76,14 @@ var state = globalSingleton("state", () => ({
|
|
|
76
76
|
starlensCalls: [],
|
|
77
77
|
starlensReplies: [],
|
|
78
78
|
starlensDefaultReply: null,
|
|
79
|
+
controls: {
|
|
80
|
+
seed: {},
|
|
81
|
+
values: {},
|
|
82
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
83
|
+
lastUsage: null,
|
|
84
|
+
writes: [],
|
|
85
|
+
refusal: null
|
|
86
|
+
},
|
|
79
87
|
allowNetwork: false
|
|
80
88
|
}));
|
|
81
89
|
|
package/dist/setup.js
CHANGED
|
@@ -108,6 +108,14 @@ var state = globalSingleton("state", () => ({
|
|
|
108
108
|
starlensCalls: [],
|
|
109
109
|
starlensReplies: [],
|
|
110
110
|
starlensDefaultReply: null,
|
|
111
|
+
controls: {
|
|
112
|
+
seed: {},
|
|
113
|
+
values: {},
|
|
114
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
115
|
+
lastUsage: null,
|
|
116
|
+
writes: [],
|
|
117
|
+
refusal: null
|
|
118
|
+
},
|
|
111
119
|
allowNetwork: false
|
|
112
120
|
}));
|
|
113
121
|
function requireDb() {
|
|
@@ -3010,6 +3018,91 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
3010
3018
|
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
3011
3019
|
}
|
|
3012
3020
|
|
|
3021
|
+
// src/simulator/controls.ts
|
|
3022
|
+
var USER_TOKEN_HEADER = "x-stardeck-user-token";
|
|
3023
|
+
var REFUSAL_STATUS = {
|
|
3024
|
+
NOT_WRITABLE: 400,
|
|
3025
|
+
INVALID_VALUE: 400,
|
|
3026
|
+
UNAUTHENTICATED: 401,
|
|
3027
|
+
FORBIDDEN: 403,
|
|
3028
|
+
WRITE_NOT_ALLOWED_HERE: 403,
|
|
3029
|
+
PROVIDER_NOT_READY: 409,
|
|
3030
|
+
UNAVAILABLE: 503
|
|
3031
|
+
};
|
|
3032
|
+
var controlsModule = null;
|
|
3033
|
+
function loadControls() {
|
|
3034
|
+
controlsModule ??= import("@stardeck-customer-apps/controls");
|
|
3035
|
+
return controlsModule;
|
|
3036
|
+
}
|
|
3037
|
+
function currentControlsBody() {
|
|
3038
|
+
const controls = {};
|
|
3039
|
+
for (const [key, value] of Object.entries(state.controls.values)) {
|
|
3040
|
+
controls[key] = state.controls.unavailable.has(key) ? { ok: false, reason: "unavailable (simulated)" } : { ok: true, value };
|
|
3041
|
+
}
|
|
3042
|
+
return controls;
|
|
3043
|
+
}
|
|
3044
|
+
function refusalResponse(refusal) {
|
|
3045
|
+
return json({ error: refusal }, REFUSAL_STATUS[refusal.code]);
|
|
3046
|
+
}
|
|
3047
|
+
function handleSync(body) {
|
|
3048
|
+
const usage = body.usage;
|
|
3049
|
+
if (typeof usage === "object" && usage !== null) {
|
|
3050
|
+
const locations = usage.locations;
|
|
3051
|
+
state.controls.lastUsage = typeof locations === "number" && Number.isInteger(locations) && locations >= 0 ? { locations } : {};
|
|
3052
|
+
}
|
|
3053
|
+
return json({ controls: currentControlsBody() });
|
|
3054
|
+
}
|
|
3055
|
+
async function handleWrite(request, key) {
|
|
3056
|
+
const { CONTROLS, isWritableControl } = await loadControls();
|
|
3057
|
+
if (!isWritableControl(key)) {
|
|
3058
|
+
return refusalResponse({
|
|
3059
|
+
code: "NOT_WRITABLE",
|
|
3060
|
+
message: `${key} cannot be changed from the app`
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
const userToken = request.headers.get(USER_TOKEN_HEADER);
|
|
3064
|
+
if (!userToken) {
|
|
3065
|
+
return refusalResponse({
|
|
3066
|
+
code: "UNAUTHENTICATED",
|
|
3067
|
+
message: "A signed-in person is required (X-Stardeck-User-Token)"
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
const body = await readJsonBody(request);
|
|
3071
|
+
const parsed = CONTROLS[key].schema.safeParse(body.value);
|
|
3072
|
+
if (!parsed.success) {
|
|
3073
|
+
return refusalResponse({
|
|
3074
|
+
code: "INVALID_VALUE",
|
|
3075
|
+
message: parsed.error.issues[0]?.message ?? "Invalid value"
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
const refusal = state.controls.refusal;
|
|
3079
|
+
if (refusal) {
|
|
3080
|
+
state.controls.refusal = null;
|
|
3081
|
+
return refusalResponse(refusal);
|
|
3082
|
+
}
|
|
3083
|
+
state.controls.values[key] = parsed.data;
|
|
3084
|
+
state.controls.unavailable.delete(key);
|
|
3085
|
+
state.controls.writes.push({ key, value: parsed.data, userToken });
|
|
3086
|
+
return json({ controls: currentControlsBody() });
|
|
3087
|
+
}
|
|
3088
|
+
var CONTROLS_PATH = /^\/api\/deployments\/[^/]+\/controls(?:\/([^/]+))?$/;
|
|
3089
|
+
async function handleControlsRequest(request, url) {
|
|
3090
|
+
const match = url.pathname.match(CONTROLS_PATH);
|
|
3091
|
+
const key = match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
3092
|
+
if (!key && request.method === "POST") {
|
|
3093
|
+
return handleSync(await readJsonBody(request));
|
|
3094
|
+
}
|
|
3095
|
+
if (key && request.method === "PUT") {
|
|
3096
|
+
return handleWrite(request, key);
|
|
3097
|
+
}
|
|
3098
|
+
return json(
|
|
3099
|
+
{
|
|
3100
|
+
error: { code: "UNAVAILABLE", message: `No simulator for ${request.method} ${url.pathname}` }
|
|
3101
|
+
},
|
|
3102
|
+
404
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3013
3106
|
// src/simulator/schedules.ts
|
|
3014
3107
|
async function handleScheduleWake(request, scheduleKey) {
|
|
3015
3108
|
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
@@ -3158,8 +3251,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3158
3251
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3159
3252
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3160
3253
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3161
|
-
|
|
3254
|
+
const controlsMatch = url.pathname.match(CONTROLS_PATH2);
|
|
3255
|
+
return !!(controlsMatch || isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3162
3256
|
}
|
|
3257
|
+
var CONTROLS_PATH2 = /^\/api\/deployments\/[^/]+\/controls(\/[^/]+)?$/;
|
|
3163
3258
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3164
3259
|
function deploymentIdFromPath(url) {
|
|
3165
3260
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -3227,6 +3322,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3227
3322
|
if (edgeMatch) {
|
|
3228
3323
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
3229
3324
|
}
|
|
3325
|
+
if (CONTROLS_PATH2.test(url.pathname)) {
|
|
3326
|
+
return handleControlsRequest(request, url);
|
|
3327
|
+
}
|
|
3230
3328
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3231
3329
|
if (scheduleWakeMatch) {
|
|
3232
3330
|
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
@@ -3255,7 +3353,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3255
3353
|
}
|
|
3256
3354
|
}
|
|
3257
3355
|
return failure(
|
|
3258
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
3356
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, app controls sync/write, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
3259
3357
|
404
|
|
3260
3358
|
);
|
|
3261
3359
|
}
|
package/dist/setup.mjs
CHANGED
|
@@ -84,6 +84,14 @@ var state = globalSingleton("state", () => ({
|
|
|
84
84
|
starlensCalls: [],
|
|
85
85
|
starlensReplies: [],
|
|
86
86
|
starlensDefaultReply: null,
|
|
87
|
+
controls: {
|
|
88
|
+
seed: {},
|
|
89
|
+
values: {},
|
|
90
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
91
|
+
lastUsage: null,
|
|
92
|
+
writes: [],
|
|
93
|
+
refusal: null
|
|
94
|
+
},
|
|
87
95
|
allowNetwork: false
|
|
88
96
|
}));
|
|
89
97
|
function requireDb() {
|
|
@@ -2986,6 +2994,91 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2986
2994
|
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
2987
2995
|
}
|
|
2988
2996
|
|
|
2997
|
+
// src/simulator/controls.ts
|
|
2998
|
+
var USER_TOKEN_HEADER = "x-stardeck-user-token";
|
|
2999
|
+
var REFUSAL_STATUS = {
|
|
3000
|
+
NOT_WRITABLE: 400,
|
|
3001
|
+
INVALID_VALUE: 400,
|
|
3002
|
+
UNAUTHENTICATED: 401,
|
|
3003
|
+
FORBIDDEN: 403,
|
|
3004
|
+
WRITE_NOT_ALLOWED_HERE: 403,
|
|
3005
|
+
PROVIDER_NOT_READY: 409,
|
|
3006
|
+
UNAVAILABLE: 503
|
|
3007
|
+
};
|
|
3008
|
+
var controlsModule = null;
|
|
3009
|
+
function loadControls() {
|
|
3010
|
+
controlsModule ??= import("@stardeck-customer-apps/controls");
|
|
3011
|
+
return controlsModule;
|
|
3012
|
+
}
|
|
3013
|
+
function currentControlsBody() {
|
|
3014
|
+
const controls = {};
|
|
3015
|
+
for (const [key, value] of Object.entries(state.controls.values)) {
|
|
3016
|
+
controls[key] = state.controls.unavailable.has(key) ? { ok: false, reason: "unavailable (simulated)" } : { ok: true, value };
|
|
3017
|
+
}
|
|
3018
|
+
return controls;
|
|
3019
|
+
}
|
|
3020
|
+
function refusalResponse(refusal) {
|
|
3021
|
+
return json({ error: refusal }, REFUSAL_STATUS[refusal.code]);
|
|
3022
|
+
}
|
|
3023
|
+
function handleSync(body) {
|
|
3024
|
+
const usage = body.usage;
|
|
3025
|
+
if (typeof usage === "object" && usage !== null) {
|
|
3026
|
+
const locations = usage.locations;
|
|
3027
|
+
state.controls.lastUsage = typeof locations === "number" && Number.isInteger(locations) && locations >= 0 ? { locations } : {};
|
|
3028
|
+
}
|
|
3029
|
+
return json({ controls: currentControlsBody() });
|
|
3030
|
+
}
|
|
3031
|
+
async function handleWrite(request, key) {
|
|
3032
|
+
const { CONTROLS, isWritableControl } = await loadControls();
|
|
3033
|
+
if (!isWritableControl(key)) {
|
|
3034
|
+
return refusalResponse({
|
|
3035
|
+
code: "NOT_WRITABLE",
|
|
3036
|
+
message: `${key} cannot be changed from the app`
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
const userToken = request.headers.get(USER_TOKEN_HEADER);
|
|
3040
|
+
if (!userToken) {
|
|
3041
|
+
return refusalResponse({
|
|
3042
|
+
code: "UNAUTHENTICATED",
|
|
3043
|
+
message: "A signed-in person is required (X-Stardeck-User-Token)"
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
const body = await readJsonBody(request);
|
|
3047
|
+
const parsed = CONTROLS[key].schema.safeParse(body.value);
|
|
3048
|
+
if (!parsed.success) {
|
|
3049
|
+
return refusalResponse({
|
|
3050
|
+
code: "INVALID_VALUE",
|
|
3051
|
+
message: parsed.error.issues[0]?.message ?? "Invalid value"
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
const refusal = state.controls.refusal;
|
|
3055
|
+
if (refusal) {
|
|
3056
|
+
state.controls.refusal = null;
|
|
3057
|
+
return refusalResponse(refusal);
|
|
3058
|
+
}
|
|
3059
|
+
state.controls.values[key] = parsed.data;
|
|
3060
|
+
state.controls.unavailable.delete(key);
|
|
3061
|
+
state.controls.writes.push({ key, value: parsed.data, userToken });
|
|
3062
|
+
return json({ controls: currentControlsBody() });
|
|
3063
|
+
}
|
|
3064
|
+
var CONTROLS_PATH = /^\/api\/deployments\/[^/]+\/controls(?:\/([^/]+))?$/;
|
|
3065
|
+
async function handleControlsRequest(request, url) {
|
|
3066
|
+
const match = url.pathname.match(CONTROLS_PATH);
|
|
3067
|
+
const key = match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
3068
|
+
if (!key && request.method === "POST") {
|
|
3069
|
+
return handleSync(await readJsonBody(request));
|
|
3070
|
+
}
|
|
3071
|
+
if (key && request.method === "PUT") {
|
|
3072
|
+
return handleWrite(request, key);
|
|
3073
|
+
}
|
|
3074
|
+
return json(
|
|
3075
|
+
{
|
|
3076
|
+
error: { code: "UNAVAILABLE", message: `No simulator for ${request.method} ${url.pathname}` }
|
|
3077
|
+
},
|
|
3078
|
+
404
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
|
|
2989
3082
|
// src/simulator/schedules.ts
|
|
2990
3083
|
async function handleScheduleWake(request, scheduleKey) {
|
|
2991
3084
|
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
@@ -3134,8 +3227,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3134
3227
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3135
3228
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3136
3229
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3137
|
-
|
|
3230
|
+
const controlsMatch = url.pathname.match(CONTROLS_PATH2);
|
|
3231
|
+
return !!(controlsMatch || isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3138
3232
|
}
|
|
3233
|
+
var CONTROLS_PATH2 = /^\/api\/deployments\/[^/]+\/controls(\/[^/]+)?$/;
|
|
3139
3234
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3140
3235
|
function deploymentIdFromPath(url) {
|
|
3141
3236
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -3203,6 +3298,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3203
3298
|
if (edgeMatch) {
|
|
3204
3299
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
3205
3300
|
}
|
|
3301
|
+
if (CONTROLS_PATH2.test(url.pathname)) {
|
|
3302
|
+
return handleControlsRequest(request, url);
|
|
3303
|
+
}
|
|
3206
3304
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3207
3305
|
if (scheduleWakeMatch) {
|
|
3208
3306
|
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
@@ -3231,7 +3329,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3231
3329
|
}
|
|
3232
3330
|
}
|
|
3233
3331
|
return failure(
|
|
3234
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
3332
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, app controls sync/write, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
3235
3333
|
404
|
|
3236
3334
|
);
|
|
3237
3335
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"@electric-sql/pglite": "^0.3.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
+
"@stardeck-customer-apps/controls": ">=0.1.0",
|
|
68
69
|
"@stardeck-customer-apps/edge-sdk": ">=1.4.0",
|
|
69
70
|
"@stardeck-customer-apps/integrations-sdk": ">=1.16.0",
|
|
70
71
|
"@stardeck-customer-apps/payments-sdk": ">=0.1.0",
|
|
@@ -74,6 +75,9 @@
|
|
|
74
75
|
"@stardeck-customer-apps/starlens": ">=0.1.0"
|
|
75
76
|
},
|
|
76
77
|
"peerDependenciesMeta": {
|
|
78
|
+
"@stardeck-customer-apps/controls": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
77
81
|
"@stardeck-customer-apps/integrations-sdk": {
|
|
78
82
|
"optional": true
|
|
79
83
|
},
|
|
@@ -95,6 +99,7 @@
|
|
|
95
99
|
},
|
|
96
100
|
"devDependencies": {
|
|
97
101
|
"@neondatabase/serverless": "^1.0.0",
|
|
102
|
+
"@stardeck-customer-apps/controls": "*",
|
|
98
103
|
"@stardeck-customer-apps/data-store-sdk": "*",
|
|
99
104
|
"@stardeck-customer-apps/edge-sdk": "*",
|
|
100
105
|
"@stardeck-customer-apps/email-sdk": "*",
|