@assembly-js/app-foundation 0.0.1 → 0.0.2-rc.3

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/CLAUDE.md CHANGED
@@ -23,11 +23,22 @@ Assembly-specific gotchas live in `.claude/skills/<name>/SKILL.md`. Invoke them
23
23
  - `assembly-custom-fields` — workspace-defined fields, multiSelect rules
24
24
  - `assembly-native-resources` — native message channels, forms, portal files/file channels: never hardcode their IDs, discover them at runtime
25
25
  - `assembly-mutations` — minimal `requestBody` rule for `update*` calls
26
+ <!-- !feature:notification-settings -->
27
+ - `assembly-notifications` — in-product + email notifications via createNotification; `ctaParams` for notification CTA URLs (the `/n` surface renders from those, not the best-effort `notificationId` claim)
28
+ <!-- /feature:notification-settings -->
29
+ <!-- feature:notification-settings -->
30
+ - `assembly-notifications` — in-product + email notifications via createNotification; `ctaParams` for notification CTA URLs (the `/n` surface renders from those, not the best-effort `notificationId` claim); per-user toggleable categories via manifest `notificationSettings` + `notificationSettingId`
31
+ <!-- /feature:notification-settings -->
26
32
  - `clearing-client-notifications` — REQUIRED when the app sends client notifications: clear the portal sidebar badge with markNotificationRead (clear-on-view vs clear-on-action)
33
+ <!-- feature:api-resources -->
34
+ - `assembly-automations` — declare the app's API contract in `.assembly/api-resources.json` (a separate file from `app.json`): domain events that become automation triggers + workspace webhooks, and staff-invokable operations that become automation actions. Declared events must also be emitted at runtime, reporting the acting user on the documented headers — declaring alone never fires anything. Read it before adding either.
35
+ <!-- /feature:api-resources -->
36
+ - `assembly-app-surfaces` — REQUIRED before writing `.assembly/app.json`: declare which surfaces (dashboard, client portal, notification) the app exposes; omitting the client surface keeps the app off client portal nav entirely
27
37
  - `assembly-app-bridge-header` — breadcrumbs (don't include app name), CTAs
28
38
  - `assembly-app-bridge-toast` — show toast notifications in the parent dashboard
29
39
  - `assembly-ai` — AI features via Assembly's OpenAI proxy (no API keys needed)
30
40
  - `assembly-storage` — app-private file uploads/downloads (NOT the portal Files module). The storage helpers come from `@assembly-js/app-foundation/server` — import them, don't rewrite them. POST only `{ fileName }` (a bare name) and store the key the platform returns; never build the S3 key yourself.
41
+ - `assembly-integrations` (present only when third-party integrations are enabled for this workspace) — third-party providers (Slack, etc.) via `requiredIntegrations` + `getIntegration()`. If this skill is present, read it before declaring an integration or writing any provider call — it lists what's connectable and each provider's gotchas. If it is absent, managed OAuth integrations are not available here; use `requiredSecrets` for any external service
31
42
  - `assembly-avatar-sub` — row-duplication + AvatarSub pattern for showing which company a client row represents (multi-company clients)
32
43
  - `assembly-user-company-selector` — UserCompanySelector for picking clients, internal users, companies
33
44
  - `assembly-forms` — react-hook-form + zod + Field component validation pattern
@@ -226,7 +237,12 @@ Any external service (SendGrid, Twilio, Resend, a niche SaaS, an API-key-only se
226
237
  - `type` — `url | api_key | secret | text`. The platform picks input style from this.
227
238
  - `docsUrl` — optional link the platform renders as "Where do I find this?".
228
239
 
240
+ <!-- !feature:notification-settings -->
229
241
  Valid top-level manifest keys: `surfaces`, `requiredSecrets`, and `requiredIntegrations` (leave it in place if present — it is platform-managed).
242
+ <!-- /feature:notification-settings -->
243
+ <!-- feature:notification-settings -->
244
+ Valid top-level manifest keys: `surfaces`, `requiredSecrets`, `notificationSettings` (per-user toggleable internal-user notification categories, plus an optional app-level `actionLabel` that gives the app a row in the *client's* Your Actions card — see the `assembly-notifications` skill), and `requiredIntegrations` (leave it in place if present — it is platform-managed).
245
+ <!-- /feature:notification-settings -->
230
246
 
231
247
  ### Example: post to a Slack webhook from a route handler
232
248
 
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.0.1",
3
+ "version": "0.0.2-rc.3",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
8
8
  "skills",
9
- "CLAUDE.md"
9
+ "CLAUDE.md",
10
+ "render-context.mjs"
10
11
  ],
11
12
  "exports": {
12
13
  "./server": {
@@ -0,0 +1,96 @@
1
+ // Renders this package's prose for one app: strips the feature-gate blocks the
2
+ // app's flags don't enable, and substitutes the catalog tokens.
3
+ //
4
+ // A port of prompts.RenderFeatureGates / parseFeatureMarker in the Go tree. The
5
+ // duplication is deliberate — the gates interleave with prose line by line, so the
6
+ // side that writes the files has to do the stripping, and that side is now the
7
+ // sandbox. render-context.test.mjs pins the two implementations to a shared fixture
8
+ // so a divergence fails a build rather than silently shipping wrong prose.
9
+ //
10
+ // Catalogs arrive pre-rendered from Go instead of as data: the action catalog
11
+ // branches on advertised resolvers, cross-provider resolvers, and a no-resolver
12
+ // fallback, and re-deriving that here would be a second implementation of logic
13
+ // that exists in one place today.
14
+
15
+ // A marker is a full line, optionally prefixed with "# " so it can sit inside YAML
16
+ // frontmatter without breaking the parse.
17
+ function parseFeatureMarker(line) {
18
+ let trimmed = line.trim();
19
+ if (trimmed.startsWith('# ')) trimmed = trimmed.slice(2);
20
+ if (!trimmed.startsWith('<!-- ') || !trimmed.endsWith(' -->')) return null;
21
+
22
+ let inner = trimmed.slice('<!-- '.length, -' -->'.length);
23
+ const marker = { name: '', close: false, negated: false };
24
+
25
+ if (inner.startsWith('/')) {
26
+ marker.close = true;
27
+ // Tolerate "/!feature:…" so a negated block can be closed symmetrically;
28
+ // polarity comes from the open marker either way.
29
+ inner = inner.slice(1);
30
+ if (inner.startsWith('!')) inner = inner.slice(1);
31
+ } else if (inner.startsWith('!')) {
32
+ marker.negated = true;
33
+ inner = inner.slice(1);
34
+ }
35
+
36
+ if (!inner.startsWith('feature:')) return null;
37
+ marker.name = inner.slice('feature:'.length);
38
+ return marker;
39
+ }
40
+
41
+ // Keeps the lines this app's features select. Throws on a malformed gate rather than
42
+ // guessing: a mismatched marker means the prose is not what its author intended, and
43
+ // shipping half a gated block is worse than failing the turn.
44
+ export function renderFeatureGates(content, features) {
45
+ if (!content.includes('feature:')) return content;
46
+
47
+ const enabled = new Set(features ?? []);
48
+ const out = [];
49
+ let open = null;
50
+ let include = true;
51
+
52
+ // Split keeping line terminators so the output is byte-identical to the input
53
+ // outside the stripped blocks.
54
+ const lines = content.split(/(?<=\n)/);
55
+ for (let i = 0; i < lines.length; i += 1) {
56
+ const line = lines[i];
57
+ const marker = parseFeatureMarker(line);
58
+ if (!marker) {
59
+ if (include) out.push(line);
60
+ continue;
61
+ }
62
+ if (marker.close) {
63
+ if (!open || open.name !== marker.name) {
64
+ throw new Error(`line ${i + 1}: unmatched close marker for feature "${marker.name}"`);
65
+ }
66
+ open = null;
67
+ include = true;
68
+ continue;
69
+ }
70
+ if (open) {
71
+ throw new Error(`line ${i + 1}: nested open marker for feature "${marker.name}" (blocks cannot nest)`);
72
+ }
73
+ open = marker;
74
+ include = enabled.has(marker.name) !== marker.negated;
75
+ }
76
+ if (open) {
77
+ throw new Error(`unclosed gate marker for feature "${open.name}"`);
78
+ }
79
+ return out.join('');
80
+ }
81
+
82
+ // Replaces each catalog token with the markdown Go rendered for it. A token with no
83
+ // entry is left alone: the config omits catalogs when the workspace cannot reach a
84
+ // provider, and the surrounding prose is written to read correctly either way.
85
+ export function substituteCatalogs(content, catalogs) {
86
+ let out = content;
87
+ for (const [token, markdown] of Object.entries(catalogs ?? {})) {
88
+ out = out.split(token).join(markdown);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ // The whole render for one file.
94
+ export function renderContextFile(content, config) {
95
+ return substituteCatalogs(renderFeatureGates(content, config?.features), config?.catalogs);
96
+ }
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: assembly-app-surfaces
3
+ description: Use whenever generating a Vibe app to declare which surfaces (dashboard-internal, client portal, or both) the app exposes. Omitting the client surface keeps the app off client portal nav entirely. The choice is a product decision, not a default.
4
+ ---
5
+
6
+ Every generated app must declare its **surfaces** in a `.assembly/app.json` file at the repo root. Note: `surfaces` declares surface *types* (dashboard, client portal, notification) — per-client or per-company visibility is a platform setting configured in the app's settings, not in this file. The build pipeline reads this file after deploy and uses it to populate the marketplace app's `InternalUrl` (dashboard preview) and `ClientUrl` (client portal). If you omit a surface, that URL stays empty — and an empty `ClientUrl` means the app does not appear in client portal nav at all.
7
+
8
+ ## First decide: does this app have a client view?
9
+
10
+ A Vibe app has a **client view** only if end-clients (the customers of the workspace, not the workspace's internal users) are meant to interact with it directly inside the client portal. Examples:
11
+
12
+ - **Internal-only** (omit `client`): admin dashboards, internal reporting, time trackers used only by employees, internal CRM views.
13
+ - **Client-facing** (include `client`): self-serve client request forms, client-visible status pages, client-shared file browsers.
14
+
15
+ If you are unsure, default to **internal-only**. Do not add a client surface unless the prompt clearly describes a client-facing experience.
16
+
17
+ ## File format
18
+
19
+ ```json
20
+ {
21
+ "surfaces": {
22
+ "internal": { "path": "/" },
23
+ "client": { "path": "/c" }
24
+ }
25
+ }
26
+ ```
27
+
28
+ - `internal` is **required** — every Vibe app must have a dashboard surface. `client` is optional; omit it for internal-only apps. `notification` is optional; declare it only when the app uses `ctaParams` in notifications (see below).
29
+ - `path` is the route within your app for that surface. Must start with `/`. Use `/` for the surface's root; use distinct sub-paths if both surfaces share one Next.js app.
30
+ - Generate the corresponding routes in your app code. E.g. if you declare `client.path = "/c"`, you must have a `/c` page in the app.
31
+
32
+ <!-- !feature:notification-settings -->
33
+ > **Every surface MUST be nested under `surfaces`.** A surface placed at the top level — e.g. `{ "surfaces": {...}, "notification": { "path": "/n" } }` — is silently dropped by the deploy parser, leaving that surface's URL empty and its notification links broken. The only valid top-level keys are `surfaces`, `requiredSecrets`, and `requiredIntegrations` (see below). The build validates this: `bun run build` runs `node .assembly/validate-manifest.mjs` first and **fails with a specific error** if the manifest is malformed or a key is misplaced. Run `bun run validate:manifest` to check it directly.
34
+
35
+ `requiredSecrets` and `requiredIntegrations` are **top-level siblings of `surfaces`**, not surfaces — see the secrets guidance and the `assembly-integrations` skill. The full manifest shape is:
36
+ <!-- /feature:notification-settings -->
37
+ <!-- feature:notification-settings -->
38
+ > **Every surface MUST be nested under `surfaces`.** A surface placed at the top level — e.g. `{ "surfaces": {...}, "notification": { "path": "/n" } }` — is silently dropped by the deploy parser, leaving that surface's URL empty and its notification links broken. The only valid top-level keys are `surfaces`, `requiredSecrets`, `notificationSettings`, and `requiredIntegrations` (see below). The build validates this: `bun run build` runs `node .assembly/validate-manifest.mjs` first and **fails with a specific error** if the manifest is malformed or a key is misplaced. Run `bun run validate:manifest` to check it directly.
39
+
40
+ `requiredSecrets`, `notificationSettings`, and `requiredIntegrations` are **top-level siblings of `surfaces`**, not surfaces — see the `assembly-notifications` skill, the secrets guidance, and the `assembly-integrations` skill. The full manifest shape is:
41
+ <!-- /feature:notification-settings -->
42
+
43
+ ```json
44
+ {
45
+ "surfaces": { "internal": { "path": "/" }, "client": { "path": "/c" }, "notification": { "path": "/n" } },
46
+ "requiredSecrets": [{ "key": "SENDGRID_API_KEY", "label": "SendGrid API key" }],
47
+ "requiredIntegrations": [{ "provider": "slack", "scopes": ["chat:write", "channels:read"] }]
48
+ }
49
+ ```
50
+ <!-- feature:notification-settings -->
51
+
52
+ `notificationSettings` declares per-user toggleable internal-user notification categories — schema and rules in the `assembly-notifications` skill.
53
+ <!-- /feature:notification-settings -->
54
+ <!-- feature:api-resources -->
55
+
56
+ The app's API contract (automation triggers/actions, webhook events) is NOT an `app.json` key: it lives in a separate sibling file, `.assembly/api-resources.json` — see the `assembly-automations` skill.
57
+ <!-- /feature:api-resources -->
58
+
59
+ ## The `notification` surface
60
+
61
+ When an app sends notifications with `ctaParams`, the Notification Center needs to know where to send the user when they click the notification's CTA. Declare a `notification` surface to set that destination URL.
62
+
63
+ ```json
64
+ {
65
+ "surfaces": {
66
+ "internal": { "path": "/" },
67
+ "notification": { "path": "/n" }
68
+ }
69
+ }
70
+ ```
71
+
72
+ When `notification` is declared:
73
+ - The build pipeline sets `notificationUrl` on the marketplace app to `<deployURL>/n`.
74
+ - When a user clicks a notification CTA, the portal opens the app at `notificationUrl?token=<jwt>&<ctaParams>`.
75
+ - Your `/n` page shows its context-specific content from the `ctaParams` in the URL. Those always arrive; `notificationId` from the token payload is best-effort under the session-exchange flow, so never gate the record render on it.
76
+
77
+ **Declare the `notification` surface whenever the app sends notifications with `ctaParams`.** If notifications have no `ctaParams`, the surface is optional. See the `assembly-notifications` skill for the full CTA params pattern.
78
+
79
+ ## Examples
80
+
81
+ Dashboard-only time tracker:
82
+ ```json
83
+ { "surfaces": { "internal": { "path": "/" } } }
84
+ ```
85
+
86
+ App with both surfaces in one Next.js project:
87
+ ```json
88
+ {
89
+ "surfaces": {
90
+ "internal": { "path": "/" },
91
+ "client": { "path": "/c" }
92
+ }
93
+ }
94
+ ```
95
+
96
+ App with notifications that have CTA params:
97
+ ```json
98
+ {
99
+ "surfaces": {
100
+ "internal": { "path": "/" },
101
+ "notification": { "path": "/n" }
102
+ }
103
+ }
104
+ ```