@trylight/app 0.3.7 → 0.3.8
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/invoke/server.d.ts +1 -1
- package/dist/invoke/server.js +43 -22
- package/dist/platform/contract.d.ts +7 -0
- package/dist/platform/contract.js +4 -0
- package/dist/server/env.d.ts +1 -0
- package/dist/server/env.js +4 -0
- package/package.json +10 -12
package/dist/invoke/server.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type MiiInvoke<C extends Contracts> = {
|
|
|
20
20
|
readonly handler: (request: Request) => Promise<Response>;
|
|
21
21
|
/**
|
|
22
22
|
* Start a contract from a POST server function, as the person whose request
|
|
23
|
-
* it is answering, and return the run's id without waiting for it: for work
|
|
23
|
+
* it is answering (on a public app, a visitor with no session), and return the run's id without waiting for it: for work
|
|
24
24
|
* that must be recorded with its run (a job row that names it). A page
|
|
25
25
|
* follows it with `resume(id)`. Refusals reject with InvocationError, with
|
|
26
26
|
* the codes a button's press gets.
|
package/dist/invoke/server.js
CHANGED
|
@@ -6,7 +6,8 @@ import { isSameOrigin, readCookie } from "../chat/server.js";
|
|
|
6
6
|
import { errorBody } from "../request-error.js";
|
|
7
7
|
import { isUuid } from "../uuid.js";
|
|
8
8
|
import { failedRun, InvocationError } from "./error.js";
|
|
9
|
-
import { INVOKE_BASE_PATH } from "../platform/contract.js";
|
|
9
|
+
import { APP_CALLER_HEADERS, INVOKE_BASE_PATH } from "../platform/contract.js";
|
|
10
|
+
import { env } from "../server/env.js";
|
|
10
11
|
export { InvocationError };
|
|
11
12
|
function routeOf(request) {
|
|
12
13
|
const { pathname } = new URL(request.url);
|
|
@@ -21,6 +22,14 @@ function routeOf(request) {
|
|
|
21
22
|
const [, id = ""] = read;
|
|
22
23
|
return { kind: "read", id };
|
|
23
24
|
}
|
|
25
|
+
function platformPath(route) {
|
|
26
|
+
switch (route.kind) {
|
|
27
|
+
case "start":
|
|
28
|
+
return "/invocations";
|
|
29
|
+
case "read":
|
|
30
|
+
return `/invocations/${route.id}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
24
33
|
const invokeBody = z.object({ key: z.string(), input: z.unknown() });
|
|
25
34
|
const plazaInvocation = z.object({
|
|
26
35
|
id: z.string(),
|
|
@@ -67,28 +76,40 @@ function startBody(contract, key, rawInput) {
|
|
|
67
76
|
return JSON.stringify({ contractId: contract.id, input: input.data, outputSchema: contract.outputSchema });
|
|
68
77
|
}
|
|
69
78
|
/**
|
|
70
|
-
* The
|
|
79
|
+
* The caller behind a request to this app: the session cookie, from one of
|
|
71
80
|
* this app's own pages. Every org app shares one site with every other
|
|
72
81
|
* (*.<domain>), so a page on another org's app can send this app's cookie along.
|
|
73
82
|
*/
|
|
74
|
-
async function
|
|
83
|
+
async function callerOf(auth, request) {
|
|
75
84
|
if (!isSameOrigin(request, new URL(request.url))) {
|
|
76
85
|
throw new InvocationError(403, "cross_site", "Contracts can only be invoked from this app's own pages.");
|
|
77
86
|
}
|
|
78
87
|
const token = readCookie(request.headers.get("cookie"), auth.SESSION_COOKIE);
|
|
79
|
-
if (token === null)
|
|
80
|
-
|
|
88
|
+
if (token === null) {
|
|
89
|
+
const { appSecret } = env();
|
|
90
|
+
if (appSecret === null)
|
|
91
|
+
throw new InvocationError(401, "not_signed_in", "Sign in to use this.");
|
|
92
|
+
return { kind: "visitor", appId: miiAuthConfig().appId, secret: appSecret };
|
|
93
|
+
}
|
|
81
94
|
const session = await auth.resolveSessionWithPlaza(token);
|
|
82
95
|
if (session === null)
|
|
83
96
|
throw new InvocationError(401, "sign_in_again", "Sign out and back in to use it here.");
|
|
84
|
-
return { token, plazaToken: session.plazaToken };
|
|
97
|
+
return { kind: "person", token, plazaToken: session.plazaToken };
|
|
98
|
+
}
|
|
99
|
+
function callerHeaders(caller) {
|
|
100
|
+
switch (caller.kind) {
|
|
101
|
+
case "person":
|
|
102
|
+
return { Authorization: `Bearer ${caller.plazaToken}` };
|
|
103
|
+
case "visitor":
|
|
104
|
+
return { [APP_CALLER_HEADERS.appId]: caller.appId, [APP_CALLER_HEADERS.secret]: caller.secret };
|
|
105
|
+
}
|
|
85
106
|
}
|
|
86
|
-
/** One call to the platform's contract API as the
|
|
87
|
-
async function callPlaza(auth,
|
|
107
|
+
/** One call to the platform's contract API as the caller; a refused token ends a person's session. Rejects only when the platform can't be reached. */
|
|
108
|
+
async function callPlaza(auth, caller, call) {
|
|
88
109
|
const answer = await fetch(call.target, {
|
|
89
110
|
method: call.method,
|
|
90
111
|
headers: {
|
|
91
|
-
|
|
112
|
+
...callerHeaders(caller),
|
|
92
113
|
...(call.body === null ? {} : { "Content-Type": "application/json" })
|
|
93
114
|
},
|
|
94
115
|
body: call.body,
|
|
@@ -96,17 +117,17 @@ async function callPlaza(auth, person, call) {
|
|
|
96
117
|
redirect: "manual",
|
|
97
118
|
signal: call.signal
|
|
98
119
|
});
|
|
99
|
-
if (answer.status === 401 || answer.status === 403) {
|
|
100
|
-
await auth.revokeSession(
|
|
120
|
+
if (caller.kind === "person" && (answer.status === 401 || answer.status === 403)) {
|
|
121
|
+
await auth.revokeSession(caller.token, answer.status === 403 ? "not_a_member" : "token_rejected");
|
|
101
122
|
}
|
|
102
123
|
return answer;
|
|
103
124
|
}
|
|
104
125
|
const plazaTarget = (path) => new URL(`${miiAuthConfig().plazaUrl}/api/app-contracts/v1${path}`);
|
|
105
|
-
/** One call to the platform as the
|
|
106
|
-
async function askPlaza(auth,
|
|
126
|
+
/** One call to the platform as the caller, read as JSON. A refusal, or no answer, rejects with InvocationError. */
|
|
127
|
+
async function askPlaza(auth, caller, call) {
|
|
107
128
|
let answer;
|
|
108
129
|
try {
|
|
109
|
-
answer = await callPlaza(auth,
|
|
130
|
+
answer = await callPlaza(auth, caller, call);
|
|
110
131
|
}
|
|
111
132
|
catch (error) {
|
|
112
133
|
if (call.signal !== null && call.signal.aborted)
|
|
@@ -143,10 +164,10 @@ export function createMiiInvoke(auth, contracts, { reportError = (error, context
|
|
|
143
164
|
if (route.kind === "read" && !isUuid(route.id)) {
|
|
144
165
|
return refuse(404, "not_found", "Invocation not found.");
|
|
145
166
|
}
|
|
146
|
-
let
|
|
167
|
+
let caller;
|
|
147
168
|
let body = null;
|
|
148
169
|
try {
|
|
149
|
-
|
|
170
|
+
caller = await callerOf(auth, request);
|
|
150
171
|
if (route.kind === "start")
|
|
151
172
|
body = await readInvoke(request, declared);
|
|
152
173
|
}
|
|
@@ -155,7 +176,7 @@ export function createMiiInvoke(auth, contracts, { reportError = (error, context
|
|
|
155
176
|
throw error;
|
|
156
177
|
return refuse(error.status, error.code, error.message);
|
|
157
178
|
}
|
|
158
|
-
const target = plazaTarget(route
|
|
179
|
+
const target = plazaTarget(platformPath(route));
|
|
159
180
|
// How long the platform may hold a read of a pending invocation. This app's own
|
|
160
181
|
// function limit has to outlast it (see the README).
|
|
161
182
|
const wait = new URL(request.url).searchParams.get("wait");
|
|
@@ -164,7 +185,7 @@ export function createMiiInvoke(auth, contracts, { reportError = (error, context
|
|
|
164
185
|
let answer;
|
|
165
186
|
try {
|
|
166
187
|
// The browser giving up ends the platform's hold too.
|
|
167
|
-
answer = await callPlaza(auth,
|
|
188
|
+
answer = await callPlaza(auth, caller, { method: request.method, target, body, signal: request.signal });
|
|
168
189
|
}
|
|
169
190
|
catch (error) {
|
|
170
191
|
if (!request.signal.aborted)
|
|
@@ -190,17 +211,17 @@ export function createMiiInvoke(auth, contracts, { reportError = (error, context
|
|
|
190
211
|
return respond(answer.status, plazaInvocation.parse(await answer.json()), declared, reportError);
|
|
191
212
|
};
|
|
192
213
|
const start = async ({ request, key, input, signal = null }) => {
|
|
193
|
-
const
|
|
214
|
+
const caller = await callerOf(auth, request);
|
|
194
215
|
const body = startBody(declared.get(key), key, input);
|
|
195
|
-
const answer = await askPlaza(auth,
|
|
216
|
+
const answer = await askPlaza(auth, caller, { method: "POST", target: plazaTarget("/invocations"), body, signal });
|
|
196
217
|
return { id: plazaInvocation.parse(answer).id };
|
|
197
218
|
};
|
|
198
219
|
const result = async (key, id, request) => {
|
|
199
|
-
const
|
|
220
|
+
const caller = await callerOf(auth, request);
|
|
200
221
|
const contract = contracts[key];
|
|
201
222
|
if (!isUuid(id))
|
|
202
223
|
throw new InvocationError(404, "not_found", "Invocation not found.");
|
|
203
|
-
const invocation = plazaInvocation.parse(await askPlaza(auth,
|
|
224
|
+
const invocation = plazaInvocation.parse(await askPlaza(auth, caller, { method: "GET", target: plazaTarget(`/invocations/${id}`), body: null, signal: null }));
|
|
204
225
|
if (invocation.contractId !== contract.id) {
|
|
205
226
|
throw new InvocationError(404, "not_found", `Invocation ${id} is not a run of "${key}".`);
|
|
206
227
|
}
|
|
@@ -16,6 +16,13 @@ export declare const PLATFORM_AUTH_PATHS: {
|
|
|
16
16
|
/** "Continue with Google / Microsoft": the platform signs the person in and returns with a one-time code. */
|
|
17
17
|
socialSignIn: string;
|
|
18
18
|
};
|
|
19
|
+
/** An app's server calling the platform as the app, for a visitor with no session: which app, and its secret. */
|
|
20
|
+
export declare const APP_CALLER_HEADERS: {
|
|
21
|
+
appId: string;
|
|
22
|
+
secret: string;
|
|
23
|
+
};
|
|
24
|
+
/** The server env that secret is provisioned in. */
|
|
25
|
+
export declare const APP_SECRET_ENV = "PLATFORM_APP_SECRET";
|
|
19
26
|
/** The platform's chat API: an app's chat route forwards any path beneath it, so a new widget route needs no SDK release. */
|
|
20
27
|
export declare const PLATFORM_CHAT_PATH = "/api/app-chat/v1/chat";
|
|
21
28
|
/** What this app's chat server forwards beyond the first contract: the widget, served to every app version, offers only these. */
|
|
@@ -22,6 +22,10 @@ export const PLATFORM_AUTH_PATHS = {
|
|
|
22
22
|
/** "Continue with Google / Microsoft": the platform signs the person in and returns with a one-time code. */
|
|
23
23
|
socialSignIn: "/api/app-auth/social"
|
|
24
24
|
};
|
|
25
|
+
/** An app's server calling the platform as the app, for a visitor with no session: which app, and its secret. */
|
|
26
|
+
export const APP_CALLER_HEADERS = { appId: "x-app-id", secret: "x-app-secret" };
|
|
27
|
+
/** The server env that secret is provisioned in. */
|
|
28
|
+
export const APP_SECRET_ENV = "PLATFORM_APP_SECRET";
|
|
25
29
|
/** The platform's chat API: an app's chat route forwards any path beneath it, so a new widget route needs no SDK release. */
|
|
26
30
|
export const PLATFORM_CHAT_PATH = "/api/app-chat/v1/chat";
|
|
27
31
|
/** What this app's chat server forwards beyond the first contract: the widget, served to every app version, offers only these. */
|
package/dist/server/env.d.ts
CHANGED
package/dist/server/env.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The server env the SDK reads, in one schema: what the platform injects
|
|
2
2
|
// (Vercel env, the sandbox's .env.local).
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
|
+
import { APP_SECRET_ENV } from "../platform/contract.js";
|
|
4
5
|
const serverEnv = z
|
|
5
6
|
.object({
|
|
6
7
|
DATABASE_URL: z.string().min(1).optional(),
|
|
@@ -8,11 +9,14 @@ const serverEnv = z
|
|
|
8
9
|
DATABASE_URL_UNPOOLED: z.string().min(1).optional(),
|
|
9
10
|
MII_PLAZA_URL: z.url().optional(),
|
|
10
11
|
MII_APP_ID: z.string().min(1).optional(),
|
|
12
|
+
/** What this app's server presents to call the platform as the app, for a visitor with no session. */
|
|
13
|
+
[APP_SECRET_ENV]: z.string().min(1).nullable().default(null),
|
|
11
14
|
NODE_ENV: z.string().optional()
|
|
12
15
|
})
|
|
13
16
|
.transform((env) => ({
|
|
14
17
|
databaseUrl: env.DATABASE_URL,
|
|
15
18
|
unpooledDatabaseUrl: env.DATABASE_URL_UNPOOLED,
|
|
19
|
+
appSecret: env[APP_SECRET_ENV],
|
|
16
20
|
/** Sign-in needs the platform, this app's id in it, and a database for sessions. */
|
|
17
21
|
configured: Boolean(env.MII_PLAZA_URL && env.MII_APP_ID && env.DATABASE_URL),
|
|
18
22
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trylight/app",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "The Light SDK every v2 org app depends on: entities (Drizzle tables with a typed client), sign-in, contracts and in-app chat.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"url": "https://github.com/shubh9/mii.git",
|
|
13
13
|
"directory": "packages/app-sdk"
|
|
14
14
|
},
|
|
15
|
-
"packageManager": "pnpm@11.0.9",
|
|
16
15
|
"files": [
|
|
17
16
|
"dist",
|
|
18
17
|
"bin"
|
|
@@ -42,15 +41,6 @@
|
|
|
42
41
|
"default": "./dist/vite.js"
|
|
43
42
|
}
|
|
44
43
|
},
|
|
45
|
-
"scripts": {
|
|
46
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
47
|
-
"typecheck": "tsc --noEmit",
|
|
48
|
-
"lint": "eslint src test --max-warnings 0",
|
|
49
|
-
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
50
|
-
"check": "tsx scripts/write-brand.ts --check && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
51
|
-
"prepack": "pnpm run build",
|
|
52
|
-
"brand": "tsx scripts/write-brand.ts"
|
|
53
|
-
},
|
|
54
44
|
"peerDependencies": {
|
|
55
45
|
"@tanstack/react-query": ">=5.69",
|
|
56
46
|
"@tanstack/react-router": ">=1.170",
|
|
@@ -84,5 +74,13 @@
|
|
|
84
74
|
"typescript-eslint": "^8.70.1",
|
|
85
75
|
"vite": "^8.2.0",
|
|
86
76
|
"zod": "^4.6.5"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
80
|
+
"typecheck": "tsc --noEmit",
|
|
81
|
+
"lint": "eslint src test --max-warnings 0",
|
|
82
|
+
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
83
|
+
"check": "tsx scripts/write-brand.ts --check && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
84
|
+
"brand": "tsx scripts/write-brand.ts"
|
|
87
85
|
}
|
|
88
|
-
}
|
|
86
|
+
}
|