@namostack/plugin-sdk 0.0.1 → 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 namostack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,32 @@
1
- Not released yet. See https://docs.namostack.com
1
+ # @namostack/plugin-sdk
2
+
3
+ Build plugins for [namostack](https://app.namostack.com): talk to the app, store data for the current user, follow its theme.
4
+
5
+ Start with the CLI, which sets this up for you: `npx @namostack/cli create`.
6
+
7
+ ```ts
8
+ import { connect } from '@namostack/plugin-sdk';
9
+
10
+ const namostack = await connect();
11
+ namostack.onThemeChange((theme) => applyTheme(theme));
12
+
13
+ await namostack.data.insert('plants', { name: 'Monstera', waterEveryDays: 7 });
14
+ const { items } = await namostack.data.find('plants', {
15
+ sort: 'updatedAt',
16
+ order: 'desc',
17
+ });
18
+ ```
19
+
20
+ In `vite.config.ts`, add the Vite plugin every namostack plugin needs:
21
+
22
+ ```ts
23
+ import { namostackPlugin } from '@namostack/plugin-sdk/vite';
24
+
25
+ export default defineConfig({ plugins: [namostackPlugin()] });
26
+ ```
27
+
28
+ The full guide: [docs.namostack.com](https://docs.namostack.com).
29
+
30
+ ## License
31
+
32
+ MIT
@@ -0,0 +1,145 @@
1
+ import { z } from "zod";
2
+ //#region ../../types/src/plugin-data.d.ts
3
+ /** What `data.find` takes in the SDK, all optional. */
4
+ declare const FindOptionsSchema: z.ZodObject<{
5
+ where: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>, Record<string, string | number | boolean | null>, Record<string, string | number | boolean | null>>>;
6
+ sort: z.ZodDefault<z.ZodEnum<["createdAt", "updatedAt"]>>;
7
+ order: z.ZodDefault<z.ZodEnum<["asc", "desc"]>>;
8
+ limit: z.ZodDefault<z.ZodNumber>;
9
+ /** Opaque: the `cursor` of the previous page. */
10
+ cursor: z.ZodOptional<z.ZodString>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ sort: "createdAt" | "updatedAt";
13
+ order: "asc" | "desc";
14
+ limit: number;
15
+ where?: Record<string, string | number | boolean | null> | undefined;
16
+ cursor?: string | undefined;
17
+ }, {
18
+ sort?: "createdAt" | "updatedAt" | undefined;
19
+ where?: Record<string, string | number | boolean | null> | undefined;
20
+ order?: "asc" | "desc" | undefined;
21
+ limit?: number | undefined;
22
+ cursor?: string | undefined;
23
+ }>;
24
+ type FindOptions = z.input<typeof FindOptionsSchema>;
25
+ //#endregion
26
+ //#region ../../types/src/bridge.d.ts
27
+ declare const BridgeErrorCodeSchema: z.ZodEnum<["permission_denied", "not_found", "validation_error", "quota_exceeded", "timeout", "upgrade_required", "internal"]>;
28
+ type BridgeErrorCode = z.infer<typeof BridgeErrorCodeSchema>;
29
+ /** Enough for a plugin using seedui: its `ThemeProvider` builds the rest from the mode. */
30
+ declare const PluginThemeSchema: z.ZodObject<{
31
+ mode: z.ZodEnum<["light", "dark"]>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ mode: "light" | "dark";
34
+ }, {
35
+ mode: "light" | "dark";
36
+ }>;
37
+ type PluginTheme = z.infer<typeof PluginThemeSchema>;
38
+ /** What the plugin learns about itself and the app once connected. */
39
+ declare const BridgeContextSchema: z.ZodObject<{
40
+ /** In live testing (`env: 'dev'`), the slug: the plugin may not be published yet. */
41
+ pluginId: z.ZodString;
42
+ slug: z.ZodString;
43
+ /** `dev` while live testing with `namostack dev`: its data is kept apart from real data. */
44
+ env: z.ZodEnum<["prod", "dev"]>;
45
+ name: z.ZodString;
46
+ version: z.ZodString;
47
+ theme: z.ZodObject<{
48
+ mode: z.ZodEnum<["light", "dark"]>;
49
+ }, "strip", z.ZodTypeAny, {
50
+ mode: "light" | "dark";
51
+ }, {
52
+ mode: "light" | "dark";
53
+ }>;
54
+ /** The browser's language, e.g. `en-US`. */
55
+ locale: z.ZodString;
56
+ }, "strip", z.ZodTypeAny, {
57
+ pluginId: string;
58
+ slug: string;
59
+ env: "prod" | "dev";
60
+ name: string;
61
+ version: string;
62
+ theme: {
63
+ mode: "light" | "dark";
64
+ };
65
+ locale: string;
66
+ }, {
67
+ pluginId: string;
68
+ slug: string;
69
+ env: "prod" | "dev";
70
+ name: string;
71
+ version: string;
72
+ theme: {
73
+ mode: "light" | "dark";
74
+ };
75
+ locale: string;
76
+ }>;
77
+ type BridgeContext = z.infer<typeof BridgeContextSchema>;
78
+ /** `user.getProfile()`: never the email. */
79
+ declare const PluginUserProfileSchema: z.ZodObject<{
80
+ displayName: z.ZodString;
81
+ }, "strip", z.ZodTypeAny, {
82
+ displayName: string;
83
+ }, {
84
+ displayName: string;
85
+ }>;
86
+ type PluginUserProfile = z.infer<typeof PluginUserProfileSchema>;
87
+ declare const PluginNotificationSchema: z.ZodObject<{
88
+ message: z.ZodString;
89
+ tone: z.ZodDefault<z.ZodEnum<["info", "success", "warning", "error"]>>;
90
+ }, "strip", z.ZodTypeAny, {
91
+ message: string;
92
+ tone: "error" | "info" | "success" | "warning";
93
+ }, {
94
+ message: string;
95
+ tone?: "error" | "info" | "success" | "warning" | undefined;
96
+ }>;
97
+ type PluginNotification = z.input<typeof PluginNotificationSchema>;
98
+ type NotificationTone = z.infer<typeof PluginNotificationSchema>['tone'];
99
+ //#endregion
100
+ //#region src/connect.d.ts
101
+ /** A stored document: the plugin's fields next to the ones namostack sets. */
102
+ type StoredDocument<TFields extends object = Record<string, unknown>> = TFields & {
103
+ id: string;
104
+ createdAt: string;
105
+ updatedAt: string;
106
+ };
107
+ interface FindResult<TFields extends object> {
108
+ items: StoredDocument<TFields>[];
109
+ /** Pass it back as `cursor` for the next page; absent once there's nothing more. */
110
+ cursor?: string;
111
+ }
112
+ /** What a plugin gets from `connect()`. Data is the current user's, for this plugin only. */
113
+ interface Namostack {
114
+ /** As it was when connecting; follow theme changes with `onThemeChange`. */
115
+ context: BridgeContext;
116
+ data: {
117
+ find: <TFields extends object = Record<string, unknown>>(collection: string, options?: FindOptions) => Promise<FindResult<TFields>>;
118
+ get: <TFields extends object = Record<string, unknown>>(collection: string, id: string) => Promise<StoredDocument<TFields>>;
119
+ insert: <TFields extends object>(collection: string, fields: TFields) => Promise<StoredDocument<TFields>>;
120
+ /** Shallow merge: the patch's top-level fields replace the document's. */
121
+ update: <TFields extends object>(collection: string, id: string, patch: Partial<TFields>) => Promise<StoredDocument<TFields>>;
122
+ delete: (collection: string, id: string) => Promise<void>;
123
+ };
124
+ user: {
125
+ getProfile: () => Promise<PluginUserProfile>;
126
+ };
127
+ ui: {
128
+ notify: (notification: PluginNotification) => Promise<void>;
129
+ /** Sets the plugin frame's height, in CSS pixels. */
130
+ resize: (height: number) => Promise<void>;
131
+ };
132
+ /** Returns a function that stops listening. */
133
+ onThemeChange: (listener: (theme: PluginTheme) => void) => () => void;
134
+ }
135
+ /** Connects to the namostack app. Call it once, when the plugin starts. */
136
+ export declare function connect(): Promise<Namostack>;
137
+ //#endregion
138
+ //#region src/errors.d.ts
139
+ /** Every failed SDK call throws this; `message` is safe to show to the user. */
140
+ export declare class BridgeError extends Error {
141
+ readonly code: BridgeErrorCode;
142
+ constructor(code: BridgeErrorCode, message: string);
143
+ }
144
+ //#endregion
145
+ export type { BridgeContext, BridgeErrorCode, FindOptions, FindResult, Namostack, NotificationTone, PluginNotification, PluginTheme, PluginUserProfile, StoredDocument };
package/dist/index.js ADDED
@@ -0,0 +1,508 @@
1
+ import { z } from "zod";
2
+ import { CallOptions, ErrorCode, PenpalError, WindowMessenger, connect as connect$1 } from "penpal";
3
+ //#region ../../types/src/auth.ts
4
+ /** Trimmed and lowercased before every lookup, so `Ada@Example.com ` and `ada@example.com` are one account. */
5
+ const EmailSchema = z.string().trim().toLowerCase().email().max(254);
6
+ /** Rules for a new password (signup, password change). Login accepts any string, so old rules never lock anyone out. */
7
+ const NewPasswordSchema = z.string().min(10, "Password must be at least 10 characters").max(256, "Password must be at most 256 characters").regex(/[a-z]/, "Password must contain a lowercase letter").regex(/[A-Z]/, "Password must contain an uppercase letter").regex(/[0-9]/, "Password must contain a digit").regex(/[^a-zA-Z0-9]/, "Password must contain a special character");
8
+ const PasswordSchema = z.string().min(1).max(256);
9
+ /** Always UTC (`…Z`): built with `Date#toISOString()`, never from a local-time string. */
10
+ const TimestampSchema = z.string().datetime();
11
+ z.object({
12
+ email: EmailSchema,
13
+ password: NewPasswordSchema,
14
+ displayName: z.string().trim().min(1).max(100)
15
+ });
16
+ z.object({
17
+ email: EmailSchema,
18
+ password: PasswordSchema
19
+ });
20
+ z.object({
21
+ current: PasswordSchema,
22
+ next: NewPasswordSchema
23
+ });
24
+ const UserSchema = z.object({
25
+ id: z.string(),
26
+ email: z.string(),
27
+ displayName: z.string()
28
+ });
29
+ z.object({ user: UserSchema });
30
+ const SessionSummarySchema = z.object({
31
+ id: z.string(),
32
+ /** Raw `User-Agent` header, shown as the device. */
33
+ userAgent: z.string().optional(),
34
+ ip: z.string().optional(),
35
+ lastSeenAt: TimestampSchema,
36
+ /** The session making this request. */
37
+ current: z.boolean()
38
+ });
39
+ z.object({ sessions: z.array(SessionSummarySchema) });
40
+ z.object({ token: z.string().min(1) });
41
+ /** A request authenticated by the session cookie (the web app). */
42
+ const SessionIdentitySchema = z.object({
43
+ kind: z.literal("session"),
44
+ userId: z.string(),
45
+ sessionId: z.string()
46
+ });
47
+ /** A request authenticated by a CLI access token (`Authorization: Bearer nst_pat_…`). */
48
+ const CliTokenIdentitySchema = z.object({
49
+ kind: z.literal("cliToken"),
50
+ userId: z.string(),
51
+ tokenId: z.string()
52
+ });
53
+ z.discriminatedUnion("kind", [SessionIdentitySchema, CliTokenIdentitySchema]);
54
+ const BridgeErrorCodeSchema = z.enum([
55
+ "permission_denied",
56
+ "not_found",
57
+ "validation_error",
58
+ "quota_exceeded",
59
+ "timeout",
60
+ "upgrade_required",
61
+ "internal"
62
+ ]);
63
+ /** How a failed call crosses the bridge; `message` is safe to show. */
64
+ const BridgeErrorBodySchema = z.object({
65
+ code: BridgeErrorCodeSchema.catch("internal"),
66
+ message: z.string()
67
+ });
68
+ z.object({ sdkVersion: z.string().min(1) });
69
+ function bridgeResultSchema(value) {
70
+ return z.discriminatedUnion("ok", [z.object({
71
+ ok: z.literal(true),
72
+ value
73
+ }), z.object({
74
+ ok: z.literal(false),
75
+ error: BridgeErrorBodySchema
76
+ })]);
77
+ }
78
+ /** Enough for a plugin using seedui: its `ThemeProvider` builds the rest from the mode. */
79
+ const PluginThemeSchema = z.object({ mode: z.enum(["light", "dark"]) });
80
+ /** What the plugin learns about itself and the app once connected. */
81
+ const BridgeContextSchema = z.object({
82
+ /** In live testing (`env: 'dev'`), the slug: the plugin may not be published yet. */
83
+ pluginId: z.string(),
84
+ slug: z.string(),
85
+ /** `dev` while live testing with `namostack dev`: its data is kept apart from real data. */
86
+ env: z.enum(["prod", "dev"]),
87
+ name: z.string(),
88
+ version: z.string(),
89
+ theme: PluginThemeSchema,
90
+ /** The browser's language, e.g. `en-US`. */
91
+ locale: z.string()
92
+ });
93
+ /** `user.getProfile()`: never the email. */
94
+ const PluginUserProfileSchema = z.object({ displayName: z.string() });
95
+ const PluginNotificationSchema = z.object({
96
+ message: z.string().trim().min(1).max(300),
97
+ tone: z.enum([
98
+ "info",
99
+ "success",
100
+ "warning",
101
+ "error"
102
+ ]).default("info")
103
+ });
104
+ /** `ui.resize(height)`, in CSS pixels. */
105
+ const FrameHeightSchema = z.number().int().min(0).max(1e5);
106
+ const DocumentIdSchema = z.string().min(1);
107
+ //#endregion
108
+ //#region ../../types/src/cli.ts
109
+ /** Shown in Settings → Developer; the CLI suggests its hostname, the user confirms it when approving. */
110
+ const TokenNameSchema = z.string().trim().min(1).max(100);
111
+ /** `WDJB-MJHT`: what the user checks in the browser against what the CLI printed. */
112
+ const UserCodeSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{4}-[A-Z]{4}$/, "Invalid code");
113
+ z.object({ tokenName: TokenNameSchema });
114
+ z.object({
115
+ /** Secret the CLI polls with; never shown to the user. */
116
+ deviceCode: z.string(),
117
+ userCode: z.string(),
118
+ /** The web app's approval page for this login. */
119
+ verifyUrl: z.string().url(),
120
+ /** Seconds to wait between polls. */
121
+ interval: z.number().int(),
122
+ /** Seconds before the login request expires. */
123
+ expiresIn: z.number().int()
124
+ });
125
+ z.object({ deviceCode: z.string().min(1) });
126
+ z.discriminatedUnion("status", [
127
+ z.object({ status: z.literal("pending") }),
128
+ z.object({
129
+ status: z.literal("approved"),
130
+ /** `nst_pat_…`: the CLI stores it, the server only keeps its hash. */
131
+ token: z.string(),
132
+ expiresAt: TimestampSchema
133
+ }),
134
+ z.object({ status: z.literal("denied") }),
135
+ z.object({ status: z.literal("expired") })
136
+ ]);
137
+ z.object({ userCode: UserCodeSchema });
138
+ z.object({
139
+ userCode: z.string(),
140
+ tokenName: z.string()
141
+ });
142
+ z.object({
143
+ userCode: UserCodeSchema,
144
+ tokenName: TokenNameSchema
145
+ });
146
+ /** A CLI access token as Settings → Developer lists it; never the token itself. */
147
+ const CliTokenSummarySchema = z.object({
148
+ id: z.string(),
149
+ name: z.string(),
150
+ /** Absent until the CLI first uses the token. */
151
+ lastUsedAt: TimestampSchema.optional(),
152
+ expiresAt: TimestampSchema
153
+ });
154
+ z.object({ tokens: z.array(CliTokenSummarySchema) });
155
+ z.object({ tokenId: z.string().min(1) });
156
+ z.object({ token: z.string().min(1) });
157
+ //#endregion
158
+ //#region ../../types/src/plugins.ts
159
+ /** `public`: in the catalog. `unlisted`: installable by direct link. `private`: owner only. */
160
+ const PluginVisibilitySchema = z.enum([
161
+ "public",
162
+ "unlisted",
163
+ "private"
164
+ ]);
165
+ /** What a plugin can ask the host for; users see the list before installing. */
166
+ const PluginPermissionSchema = z.enum([
167
+ "storage",
168
+ "user:profile:read",
169
+ "ui:notify"
170
+ ]);
171
+ /** A plugin as the catalog lists it. */
172
+ const CatalogPluginSchema = z.object({
173
+ id: z.string(),
174
+ slug: z.string(),
175
+ name: z.string(),
176
+ description: z.string(),
177
+ developerName: z.string(),
178
+ iconUrl: z.string().optional(),
179
+ isOfficial: z.boolean(),
180
+ installCount: z.number().int(),
181
+ /** The latest version's: what the user accepts when installing. */
182
+ permissions: z.array(PluginPermissionSchema)
183
+ });
184
+ z.object({ plugins: z.array(CatalogPluginSchema) });
185
+ /** An installed plugin, with what the sidebar shows of it. */
186
+ const InstalledPluginSchema = z.object({
187
+ pluginId: z.string(),
188
+ slug: z.string(),
189
+ /** `private` ones are the user's own (`/my-plugins/<slug>`). */
190
+ visibility: PluginVisibilitySchema,
191
+ name: z.string(),
192
+ iconUrl: z.string().optional(),
193
+ enabled: z.boolean(),
194
+ /** Order in the sidebar, ascending. */
195
+ position: z.number(),
196
+ installedAt: TimestampSchema,
197
+ /** The version the user installed or last updated to; newer ones wait for `update`. */
198
+ version: z.string(),
199
+ /** From that version's manifest: what the host's bridge lets it call. */
200
+ permissions: z.array(PluginPermissionSchema),
201
+ /** Present while a newer version is published. */
202
+ update: z.object({
203
+ version: z.string(),
204
+ /** Asked for by the new version only: the user must accept them, even with auto-update on. */
205
+ addedPermissions: z.array(PluginPermissionSchema)
206
+ }).optional()
207
+ });
208
+ z.object({
209
+ url: z.string().url(),
210
+ expiresAt: TimestampSchema
211
+ });
212
+ z.object({ installedPlugins: z.array(InstalledPluginSchema) });
213
+ z.object({ pluginId: z.string().min(1) });
214
+ z.object({ version: z.string().min(1) });
215
+ z.object({ pluginId: z.string().min(1) });
216
+ /** A plugin as its owner sees it in My plugins: its catalog card, plus what only the owner sees. */
217
+ const MyPluginSchema = CatalogPluginSchema.extend({
218
+ visibility: PluginVisibilitySchema,
219
+ latestVersion: z.string()
220
+ });
221
+ z.object({ plugins: z.array(MyPluginSchema) });
222
+ z.object({ visibility: PluginVisibilitySchema });
223
+ //#endregion
224
+ //#region ../../types/src/manifest.ts
225
+ /** Globally unique, and the plugin's subdomain (`<slug>.namostack-plugins.com`). */
226
+ const PluginSlugSchema = z.string().regex(/^[a-z][a-z0-9-]{2,39}$/, "Use 3–40 lowercase letters, digits or dashes, starting with a letter");
227
+ /** Plain `MAJOR.MINOR.PATCH`: no pre-release tags, no leading zeros. */
228
+ const PluginVersionSchema = z.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/, "Use MAJOR.MINOR.PATCH, e.g. 1.2.0");
229
+ const BundlePathSchema = z.string().min(1).refine((path) => !/^([/\\]|[a-zA-Z]:)/.test(path) && !/(^|[/\\])\.\.([/\\]|$)/.test(path), "Must be a relative path inside the bundle");
230
+ /** `namostack.plugin.json`: checked by the CLI, and again by core-api on publish. */
231
+ const PluginManifestSchema = z.object({
232
+ slug: PluginSlugSchema,
233
+ name: z.string().trim().min(1).max(50),
234
+ version: PluginVersionSchema,
235
+ description: z.string().trim().max(300),
236
+ /** HTML file the host frames. */
237
+ entry: BundlePathSchema.refine((path) => path.endsWith(".html"), "Must be an HTML file"),
238
+ icon: BundlePathSchema,
239
+ /** semver range of `@namostack/plugin-sdk` the plugin was built for. */
240
+ sdk: z.string().min(1),
241
+ permissions: z.array(PluginPermissionSchema).refine((permissions) => new Set(permissions).size === permissions.length, "Permissions must not repeat")
242
+ });
243
+ z.object({ slug: PluginSlugSchema });
244
+ z.object({
245
+ slug: z.string(),
246
+ /** The caller has no plugin with this slug yet, so `namostack create` can use it. */
247
+ available: z.boolean(),
248
+ /** No public or unlisted plugin uses it: the new plugin could be published as one. */
249
+ publishable: z.boolean()
250
+ });
251
+ z.object({
252
+ slug: z.string(),
253
+ latestVersion: z.string(),
254
+ visibility: PluginVisibilitySchema
255
+ });
256
+ z.object({
257
+ manifest: PluginManifestSchema,
258
+ /** First publish only, as on `complete`: a public or unlisted one fails here if its slug is taken. */
259
+ visibility: PluginVisibilitySchema.optional()
260
+ });
261
+ z.object({
262
+ /** Pre-signed `PUT` for the gzipped tar of `dist/`. */
263
+ uploadUrl: z.string().url(),
264
+ /** Seconds the URL stays valid. */
265
+ expiresIn: z.number().int()
266
+ });
267
+ z.object({
268
+ slug: PluginSlugSchema,
269
+ version: PluginVersionSchema
270
+ });
271
+ z.object({
272
+ manifest: PluginManifestSchema,
273
+ /** First publish only; later publishes keep the plugin's visibility. Defaults to `private`. */
274
+ visibility: PluginVisibilitySchema.optional()
275
+ });
276
+ z.object({
277
+ slug: z.string(),
278
+ version: z.string(),
279
+ visibility: PluginVisibilitySchema,
280
+ /** Where the plugin can be found in the web app. */
281
+ url: z.string().url()
282
+ });
283
+ z.object({
284
+ /** The manifest on the author's disk: its permissions are the ones enforced. */
285
+ manifest: PluginManifestSchema,
286
+ /** `http://<slug>.dev.localhost:<port>/`, where `namostack dev` serves the plugin. */
287
+ devUrl: z.string().url()
288
+ });
289
+ z.object({
290
+ /** The plugin's live-testing page in the web app. */
291
+ url: z.string().url(),
292
+ /** Seconds before the session ends unless the CLI sends it again. */
293
+ expiresIn: z.number().int()
294
+ });
295
+ /** A plugin in live testing, as its author's web app shows it. */
296
+ const DevSessionSchema = z.object({
297
+ slug: z.string(),
298
+ name: z.string(),
299
+ version: z.string(),
300
+ devUrl: z.string().url(),
301
+ /** From the local manifest; granted without consent, since the author tests their own plugin. */
302
+ permissions: z.array(PluginPermissionSchema),
303
+ iconUrl: z.string().optional(),
304
+ startedAt: TimestampSchema
305
+ });
306
+ z.object({ devSessions: z.array(DevSessionSchema) });
307
+ const ErrorCodeSchema = z.enum([
308
+ "unauthenticated",
309
+ "invalid_credentials",
310
+ "permission_denied",
311
+ "not_found",
312
+ "email_unavailable",
313
+ "already_installed",
314
+ "slug_taken",
315
+ "rate_limited",
316
+ "quota_exceeded",
317
+ "validation_error",
318
+ "upgrade_required",
319
+ "internal"
320
+ ]);
321
+ z.object({
322
+ code: ErrorCodeSchema.catch("internal"),
323
+ message: z.string()
324
+ });
325
+ //#endregion
326
+ //#region ../../types/src/plugin-data.ts
327
+ /** A plugin's collection, e.g. `tasks`: one per kind of document. */
328
+ const CollectionNameSchema = z.string().regex(/^[a-z0-9_-]{1,64}$/, "Use 1–64 lowercase letters, digits, dashes or underscores");
329
+ z.object({
330
+ pluginId: z.string().min(1),
331
+ collection: CollectionNameSchema
332
+ }).extend({ id: z.string().min(1) });
333
+ /** Set by plugin-api on every document, so a plugin can't write them. */
334
+ const RESERVED_DOCUMENT_FIELDS = [
335
+ "id",
336
+ "createdAt",
337
+ "updatedAt"
338
+ ];
339
+ /** What a plugin writes: insert takes the whole document, update the top-level fields it changes. */
340
+ const DocumentFieldsSchema = z.record(z.unknown()).refine((fields) => RESERVED_DOCUMENT_FIELDS.every((field) => !Object.hasOwn(fields, field)), `${RESERVED_DOCUMENT_FIELDS.join(", ")} are set by namostack`);
341
+ /** A stored document: the plugin's fields, flat, next to the ones plugin-api sets. */
342
+ const PluginDocumentSchema = z.object({
343
+ id: z.string(),
344
+ createdAt: TimestampSchema,
345
+ updatedAt: TimestampSchema
346
+ }).passthrough();
347
+ const WhereValueSchema = z.union([
348
+ z.string(),
349
+ z.number(),
350
+ z.boolean(),
351
+ z.null()
352
+ ]);
353
+ /** Top-level field → the value it must equal. */
354
+ const DocumentWhereSchema = z.record(WhereValueSchema).refine((where) => RESERVED_DOCUMENT_FIELDS.every((field) => !Object.hasOwn(where, field)), `Can't filter on ${RESERVED_DOCUMENT_FIELDS.join(", ")}`);
355
+ /** `?where=` is the same, as JSON. */
356
+ const WhereQuerySchema = z.preprocess((value) => {
357
+ if (typeof value !== "string") return value;
358
+ try {
359
+ return JSON.parse(value);
360
+ } catch {
361
+ return value;
362
+ }
363
+ }, DocumentWhereSchema);
364
+ /** What `data.find` takes in the SDK, all optional. */
365
+ const FindOptionsSchema = z.object({
366
+ where: DocumentWhereSchema.optional(),
367
+ sort: z.enum(["createdAt", "updatedAt"]).default("createdAt"),
368
+ order: z.enum(["asc", "desc"]).default("asc"),
369
+ limit: z.coerce.number().int().min(1).max(200).default(50),
370
+ /** Opaque: the `cursor` of the previous page. */
371
+ cursor: z.string().min(1).optional()
372
+ });
373
+ FindOptionsSchema.extend({ where: WhereQuerySchema.optional() });
374
+ const FindResponseSchema = z.object({
375
+ items: z.array(PluginDocumentSchema),
376
+ /** Present while there may be more matching documents. */
377
+ cursor: z.string().optional()
378
+ });
379
+ //#endregion
380
+ //#region ../../types/src/preferences.ts
381
+ /** `system` follows the OS setting (`prefers-color-scheme`). */
382
+ const ThemePreferenceSchema = z.enum([
383
+ "light",
384
+ "dark",
385
+ "system"
386
+ ]);
387
+ z.object({
388
+ theme: ThemePreferenceSchema,
389
+ sidebarCollapsed: z.boolean(),
390
+ /** Applies plugin updates on its own, except those adding permissions. */
391
+ autoUpdatePlugins: z.boolean()
392
+ });
393
+ //#endregion
394
+ //#region src/config.ts
395
+ const SECOND_MS = 1e3;
396
+ /** Every rule the SDK runs on. Durations are in milliseconds. */
397
+ const SDK_CONFIG = {
398
+ /** The only hosts a plugin talks to: the namostack app, per environment. */
399
+ appOrigins: [
400
+ "https://app.namostack.com",
401
+ "https://app.staging.namostack.com",
402
+ "https://app.namostack.localhost:5173"
403
+ ],
404
+ /** Opened anywhere but inside the app, `connect()` gives up after this. */
405
+ connectTimeoutMs: 10 * SECOND_MS,
406
+ /** Per bridge call. */
407
+ callTimeoutMs: 10 * SECOND_MS
408
+ };
409
+ //#endregion
410
+ //#region src/errors.ts
411
+ /** Every failed SDK call throws this; `message` is safe to show to the user. */
412
+ var BridgeError = class extends Error {
413
+ code;
414
+ constructor(code, message) {
415
+ super(message);
416
+ this.name = "BridgeError";
417
+ this.code = code;
418
+ }
419
+ };
420
+ //#endregion
421
+ //#region src/version.ts
422
+ /** Sent to the app when connecting, so it can turn away SDKs too old to work with it. */
423
+ const SDK_VERSION = "0.1.0-rc.1";
424
+ //#endregion
425
+ //#region src/connect.ts
426
+ const NullSchema = z.null();
427
+ /** Arguments are checked here too, so a mistake fails before crossing the bridge. */
428
+ function parseArgument(schema, value) {
429
+ const parsed = schema.safeParse(value);
430
+ if (!parsed.success) throw new BridgeError("validation_error", parsed.error.issues[0]?.message ?? "Invalid argument");
431
+ return parsed.data;
432
+ }
433
+ /** Runs one host method and unwraps its result, which is never trusted as is. */
434
+ async function call(schema, invoke) {
435
+ let answer;
436
+ try {
437
+ answer = await invoke(new CallOptions({ timeout: SDK_CONFIG.callTimeoutMs }));
438
+ } catch (error) {
439
+ if (error instanceof PenpalError && error.code === ErrorCode.MethodCallTimeout) throw new BridgeError("timeout", "namostack took too long to answer.");
440
+ throw new BridgeError("internal", "The call to namostack failed.");
441
+ }
442
+ const result = bridgeResultSchema(schema).safeParse(answer);
443
+ if (!result.success) throw new BridgeError("internal", "namostack sent an unexpected answer.");
444
+ const outcome = result.data;
445
+ if (!outcome.ok) throw new BridgeError(outcome.error.code, outcome.error.message);
446
+ return outcome.value;
447
+ }
448
+ /** Waits for the namostack app framing this plugin; only the app's own origins are accepted. */
449
+ async function connectToHost(methods) {
450
+ const notInApp = new BridgeError("internal", "Plugins only run inside the namostack app.");
451
+ if (window.parent === window) throw notInApp;
452
+ const connection = connect$1({
453
+ messenger: new WindowMessenger({
454
+ remoteWindow: window.parent,
455
+ allowedOrigins: [...SDK_CONFIG.appOrigins]
456
+ }),
457
+ methods,
458
+ timeout: SDK_CONFIG.connectTimeoutMs
459
+ });
460
+ try {
461
+ return await connection.promise;
462
+ } catch {
463
+ throw notInApp;
464
+ }
465
+ }
466
+ /** Connects to the namostack app. Call it once, when the plugin starts. */
467
+ async function connect() {
468
+ const themeListeners = /* @__PURE__ */ new Set();
469
+ const host = await connectToHost({ onThemeChange: (theme) => {
470
+ const parsed = PluginThemeSchema.safeParse(theme);
471
+ if (parsed.success) themeListeners.forEach((listener) => listener(parsed.data));
472
+ } });
473
+ return {
474
+ context: await call(BridgeContextSchema, (options) => host.getContext({ sdkVersion: SDK_VERSION }, options)),
475
+ data: {
476
+ find: async (collection, options = {}) => {
477
+ return await call(FindResponseSchema, (callOptions) => host.data.find(parseArgument(CollectionNameSchema, collection), parseArgument(FindOptionsSchema, options), callOptions));
478
+ },
479
+ get: async (collection, id) => {
480
+ return await call(PluginDocumentSchema, (options) => host.data.get(parseArgument(CollectionNameSchema, collection), parseArgument(DocumentIdSchema, id), options));
481
+ },
482
+ insert: async (collection, fields) => {
483
+ return await call(PluginDocumentSchema, (options) => host.data.insert(parseArgument(CollectionNameSchema, collection), parseArgument(DocumentFieldsSchema, fields), options));
484
+ },
485
+ update: async (collection, id, patch) => {
486
+ return await call(PluginDocumentSchema, (options) => host.data.update(parseArgument(CollectionNameSchema, collection), parseArgument(DocumentIdSchema, id), parseArgument(DocumentFieldsSchema, patch), options));
487
+ },
488
+ delete: async (collection, id) => {
489
+ await call(NullSchema, (options) => host.data.delete(parseArgument(CollectionNameSchema, collection), parseArgument(DocumentIdSchema, id), options));
490
+ }
491
+ },
492
+ user: { getProfile: () => call(PluginUserProfileSchema, (options) => host.user.getProfile(options)) },
493
+ ui: {
494
+ notify: async (notification) => {
495
+ await call(NullSchema, (options) => host.ui.notify(parseArgument(PluginNotificationSchema, notification), options));
496
+ },
497
+ resize: async (height) => {
498
+ await call(NullSchema, (options) => host.ui.resize(parseArgument(FrameHeightSchema, height), options));
499
+ }
500
+ },
501
+ onThemeChange: (listener) => {
502
+ themeListeners.add(listener);
503
+ return () => themeListeners.delete(listener);
504
+ }
505
+ };
506
+ }
507
+ //#endregion
508
+ export { BridgeError, connect };
package/dist/vite.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { Plugin } from "vite";
2
+ //#region src/vite.d.ts
3
+ /**
4
+ * Vite settings every namostack plugin needs. Bundles are served under `/<version>/`,
5
+ * so asset URLs must be relative.
6
+ */
7
+ export declare function namostackPlugin(): Plugin;
8
+ //#endregion
package/dist/vite.js ADDED
@@ -0,0 +1,13 @@
1
+ //#region src/vite.ts
2
+ /**
3
+ * Vite settings every namostack plugin needs. Bundles are served under `/<version>/`,
4
+ * so asset URLs must be relative.
5
+ */
6
+ function namostackPlugin() {
7
+ return {
8
+ name: "namostack",
9
+ config: () => ({ base: "./" })
10
+ };
11
+ }
12
+ //#endregion
13
+ export { namostackPlugin };
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@namostack/plugin-sdk",
3
- "version": "0.0.1",
4
- "description": "Not released yet: see https://docs.namostack.com",
5
- "license": "MIT"
6
- }
3
+ "version": "0.1.0-rc.1",
4
+ "description": "Build plugins for namostack: talk to the app, store data, follow its theme.",
5
+ "license": "MIT",
6
+ "homepage": "https://docs.namostack.com",
7
+ "keywords": [
8
+ "namostack",
9
+ "plugin",
10
+ "sdk"
11
+ ],
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./vite": {
19
+ "types": "./dist/vite.d.ts",
20
+ "default": "./dist/vite.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "penpal": "^8.0.1",
31
+ "zod": "^3.23.0"
32
+ },
33
+ "peerDependencies": {
34
+ "vite": "^8.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "vite": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.13.3",
43
+ "tsdown": "^0.23.1",
44
+ "typescript": "~6.0.2",
45
+ "vite": "^8.3.0",
46
+ "@namostack/tsconfig": "0.0.0",
47
+ "@namostack/types": "0.0.0"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "lint": "oxlint",
52
+ "typecheck": "tsc -b"
53
+ }
54
+ }