@mekari-officeless/sdk 0.1.0 → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@mekari-officeless/sdk` are recorded here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
6
+ project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ > **Release process** — publishing is manual this phase. Merge to `main`, run
9
+ > `npm version <patch|minor|major>`, push the commit and the tag, then
10
+ > `npm publish --access public` from a clean checkout as an existing maintainer.
11
+ > Move the entries below from _Unreleased_ into the new version heading in the same
12
+ > pull request that bumps the version.
13
+
14
+ ## [Unreleased]
15
+
16
+ ### Added
17
+
18
+ - `officeless.init({ sdkKey, gatewayUrl? })` — checks the key's shape and returns an
19
+ instance bound to it. Sends no request and returns no project data: the key's
20
+ company, project and stage stay server-side. Several instances can be held on one
21
+ page at once.
22
+ - `app.auth.login(email)` — sends a login code and returns a `sessionId`.
23
+ - `app.auth.verify(email, sessionId, code)` — confirms the code and starts the
24
+ session.
25
+ - `app.auth.user()` — returns the signed-in person from the held session, without
26
+ sending a request.
27
+ - `app.auth.refresh()` — renews the session.
28
+ - `app.auth.logout()` — ends the session. Signing out twice does not throw.
29
+ - `app.action.hitSDKFunction(name, data, withAuth?)` — runs a published automation
30
+ created with the **SDK Function** trigger and returns its output.
31
+ - `OfficelessError`, carrying a `type` (`AuthorizationError`, `ValidationError`,
32
+ `WorkflowExecutionError`, `InternalServerError`) and the HTTP `status`, so a 429
33
+ can be told apart from a 401. A company without the feature turned on answers 404
34
+ like an unknown path, so there is deliberately no separate type for it.
35
+ - The session survives a page reload: it is stored encrypted with AES-GCM, using a
36
+ non-extractable key held in IndexedDB.
37
+ - TypeScript definitions for the whole surface, bundled to ESM and CommonJS with no
38
+ runtime dependencies.
39
+
40
+ [Unreleased]: https://bitbucket.org/jojocoders/officeless-sdk/branches/compare/main
package/README.md CHANGED
@@ -1,194 +1,324 @@
1
- # @officeless/sdk
1
+ # Officeless SDK
2
2
 
3
- Official JavaScript SDK for frontend apps (React, Vue, vanilla JS) to interact with Officeless as a serverless backend via `api_v2` workflow triggers.
3
+ Run Officeless automations and sign users in, from your own web application.
4
4
 
5
- Zero dependencies. ESM-native. Works in any modern browser or Node.js 18+.
6
-
7
- ---
8
-
9
- ## Installation
5
+ - TypeScript, with full type definitions
6
+ - ESM and CommonJS
7
+ - No runtime dependencies
8
+ - **Browser only** — see [Environment support](#environment-support)
10
9
 
11
10
  ```bash
12
- npm install @officeless/sdk
11
+ npm install @mekari-officeless/sdk
13
12
  ```
14
13
 
15
14
  ---
16
15
 
17
- ## Initialization
16
+ ## Getting started
17
+
18
+ **1. Create an SDK key.** In Officeless Studio, open your project and go to
19
+ **Settings › Developer access**, then create a key. Keys are fixed to one
20
+ environment — `Development` or `Production` — when you create them, and that cannot
21
+ be changed afterwards.
18
22
 
19
- There are two ways to initialize the client.
23
+ **2. Add your domain.** On the same page, add the domain your application is served
24
+ from to **Allowed domains**. Calls from any other domain are rejected. The list is
25
+ shared by both environments of the project.
20
26
 
21
- ### Mode A: URL Map (recommended after AI porting)
27
+ **3. Set the library up.**
22
28
 
23
- When you have an `officeless.config.json` in your project root (auto-generated by the AI Porting Agent), import and pass it directly:
29
+ ```ts
30
+ import { officeless } from '@mekari-officeless/sdk'
24
31
 
25
- ```js
26
- import { OfficelessClient } from '@officeless/sdk'
27
- import config from './officeless.config.json' assert { type: 'json' }
32
+ const app = await officeless.init({
33
+ sdkKey: 'ofc_sdk_prod_0123456789abcdef0123456789abcdef',
34
+ })
28
35
 
29
- const client = OfficelessClient.fromConfig(config)
36
+ // app an instance: app.auth.login, app.auth.verify, app.action.hitSDKFunction, …
37
+ // no project id, name, or stage — setup asks Officeless nothing
30
38
  ```
31
39
 
32
- The config file provides explicit webhook URLs for every workflow, so no guessing is needed.
40
+ `init` sends no request. It checks the key's shape, keeps it with the gateway URL,
41
+ and returns. So setup cannot fail on the network and adds nothing to your startup
42
+ time — but it also cannot tell you the key is wrong. A wrong, revoked, or deleted
43
+ key, a domain that is not on the allowed list, and a company without the feature
44
+ turned on are all reported by your first real call: `login`, `verify`, `refresh`,
45
+ `logout`, or `hitSDKFunction`.
33
46
 
34
- ### Mode B: Base URL + convention
47
+ Call it once, when your application starts. In a framework that server-renders —
48
+ Next.js, Nuxt — call it from client-side code only (see
49
+ [Environment support](#environment-support)).
35
50
 
36
- If you don't have a config file, initialize with just a base URL. Workflow names are mapped to `{baseUrl}/wh/{workflowName}` automatically.
51
+ **4. Run an automation.**
37
52
 
38
- ```js
39
- import { OfficelessClient } from '@officeless/sdk'
53
+ ```ts
54
+ const result = await app.action.hitSDKFunction('calculate_payroll', { month: '2026-08' })
55
+ // result → { status: 'success', output: { total: 8500000, period: '2026-08' } }
56
+ ```
40
57
 
41
- const client = new OfficelessClient({
42
- baseUrl: 'https://api.officeless.io',
43
- apiKey: 'olk_live_xxxx', // optional
44
- projectId: '2BfWnyXYk5mz', // optional
45
- })
58
+ ---
59
+
60
+ ## Capabilities
61
+
62
+ ### `officeless.init({ sdkKey, gatewayUrl? })`
63
+
64
+ Sets the library up and returns an instance bound to one key. Sends no request, and
65
+ returns no project data — the key's company, project and stage stay server-side and
66
+ are read from the key on every later call.
67
+
68
+ The only failure it can raise is a value that is not shaped like an SDK key
69
+ (`ofc_sdk_<stage>_<32 hex characters>`), thrown as a `ValidationError` with no HTTP
70
+ status. A well-formed key that belongs to nothing passes here and fails on the first
71
+ real call.
72
+
73
+ Calling `init` again with a different key returns another, independent instance — one
74
+ page can hold several at once, and they do not share a signed-in user.
75
+
76
+ ```ts
77
+ const production = await officeless.init({ sdkKey: 'ofc_sdk_prod_…' })
78
+ const staging = await officeless.init({ sdkKey: 'ofc_sdk_dev_…' })
46
79
  ```
47
80
 
48
- Constructor options:
81
+ `gatewayUrl` is optional and defaults to `https://officeless-gateway.mekari.com/v1`.
82
+ See [Single-tenant setup](#single-tenant-setup).
49
83
 
50
- | Option | Type | Required | Description |
51
- |--------|------|----------|-------------|
52
- | `baseUrl` | `string` | yes | Officeless API base URL |
53
- | `apiKey` | `string` | no | Sent as `X-Officeless-Key` header |
54
- | `projectId` | `string` | no | Your Officeless project ID |
55
- | `workflows` | `object` | no | Explicit map of workflow name → `{ url, method }` |
84
+ ### `app.auth.login(email)`
56
85
 
57
- ---
86
+ Sends a login code to the email and returns a `sessionId` for this attempt.
58
87
 
59
- ## `OfficelessClient.fromConfig(config)`
60
-
61
- Loads client settings from a plain object or a parsed `officeless.config.json`.
62
-
63
- ```js
64
- const client = OfficelessClient.fromConfig({
65
- officeless: {
66
- project_id: '2BfWnyXYk5mz',
67
- base_url: 'https://api.officeless.io',
68
- api_key: 'olk_live_xxxx',
69
- workflows: {
70
- 'employee-list': { url: '/app/wh/abc123', method: 'post' },
71
- 'employee-create': { url: '/app/wh/def456', method: 'post' },
72
- },
73
- },
74
- })
88
+ ```ts
89
+ const { sessionId, otpExpiresAt } = await app.auth.login('user@example.com')
75
90
  ```
76
91
 
77
- Both snake_case (`base_url`, `api_key`, `project_id`) and camelCase (`baseUrl`, `apiKey`, `projectId`) keys are accepted.
92
+ This resolves the same way whether or not the address belongs to a user of your
93
+ project — and whether or not it is even a valid address. Nothing about the address
94
+ changes the response, so this call cannot be used to discover who has an account.
95
+ Do not treat a successful response as proof the address exists.
78
96
 
79
- ---
97
+ ### `app.auth.verify(email, sessionId, code)`
80
98
 
81
- ## Triggering Workflows
99
+ Confirms the code and starts the session.
82
100
 
83
- Use `client.workflow(name)` to get a `WorkflowRef`, then call `.trigger(payload)`.
101
+ ```ts
102
+ const session = await app.auth.verify('user@example.com', sessionId, '482913')
103
+ // session → { accessToken, refreshToken, expiredAt, user }
104
+ ```
84
105
 
85
- ```js
86
- const result = await client.workflow('send-welcome-email').trigger({
87
- userId: 42,
88
- email: 'user@example.com',
89
- })
106
+ The session is stored for you, so a page reload keeps the user signed in.
90
107
 
91
- console.log(result) // data returned by the workflow
108
+ ### `app.auth.user()`
109
+
110
+ Returns the signed-in person. Reads the session your application already holds and
111
+ sends no request.
112
+
113
+ ```ts
114
+ const user = await app.auth.user()
115
+ // user → { userId, email, name, companyId, companyName }
92
116
  ```
93
117
 
94
- The workflow name must match the key in `officeless.config.json` or the path segment in `{baseUrl}/wh/{name}`.
118
+ Throws `AuthorizationError` when nobody is signed in or the session has expired.
95
119
 
96
- ---
120
+ ### `app.auth.refresh()`
97
121
 
98
- ## Table CRUD
122
+ Renews the session with the renewal token.
99
123
 
100
- Use `client.table(name)` to get a `TableRef` for a named table. Each method calls a corresponding workflow: `{tableName}-list`, `{tableName}-create`, `{tableName}-update`, `{tableName}-delete`.
124
+ ```ts
125
+ const { accessToken, expiredAt } = await app.auth.refresh()
126
+ ```
101
127
 
102
- ### `list(filters?)`
128
+ There is no automatic refresh: the library does not retry a failed call by renewing
129
+ in the background. Call `refresh()` yourself — for example when a call fails with
130
+ `AuthorizationError` and `Session expired. Please log in again.`
103
131
 
104
- ```js
105
- const employees = await client.table('employee').list({ department: 'engineering' })
106
- // calls workflow: employee-list with payload: { filters: { department: 'engineering' } }
132
+ ### `app.auth.logout()`
133
+
134
+ Ends the session, on the server and locally.
135
+
136
+ ```ts
137
+ await app.auth.logout()
107
138
  ```
108
139
 
109
- ### `create(data)`
140
+ Calling it again on an already-ended session does not throw.
110
141
 
111
- ```js
112
- const newEmployee = await client.table('employee').create({
113
- name: 'Jane Doe',
114
- role: 'Engineer',
115
- })
116
- // calls workflow: employee-create with payload: { data: { name: 'Jane Doe', role: 'Engineer' } }
142
+ ### `app.action.hitSDKFunction(name, data, withAuth?)`
143
+
144
+ Runs a published automation by name and returns its output.
145
+
146
+ ```ts
147
+ const result = await app.action.hitSDKFunction('calculate_payroll', { month: '2026-08' })
117
148
  ```
118
149
 
119
- ### `update(id, data)`
150
+ `name` is the **Function Name** set on the workflow's SDK Function trigger, not the
151
+ workflow's display title. The automation must have been created with the **SDK
152
+ Function** trigger — one built with any other trigger (Form event, API call, Webhook)
153
+ is not reachable this way and reports as not found.
154
+
155
+ `output` is whatever the automation returns: any JSON value, including `null`. It is
156
+ passed through exactly as the automation produced it, so your own field names are
157
+ never rewritten. Type it if you like:
120
158
 
121
- ```js
122
- const updated = await client.table('employee').update('emp_001', { role: 'Senior Engineer' })
123
- // calls workflow: employee-update with payload: { id: 'emp_001', data: { role: 'Senior Engineer' } }
159
+ ```ts
160
+ const result = await app.action.hitSDKFunction<{ total: number }>('calculate_payroll', {
161
+ month: '2026-08',
162
+ })
163
+ result.output.total // number
124
164
  ```
125
165
 
126
- ### `delete(id)`
166
+ `withAuth` defaults to `true` and only decides whether the SDK attaches the signed-in
167
+ session to the call. Whether a session is actually _required_ is decided by the
168
+ workflow's own **Required Authorization** setting, on the server. Passing
169
+ `withAuth: false` to a workflow that requires one does not bypass it — the call is
170
+ rejected.
127
171
 
128
- ```js
129
- await client.table('employee').delete('emp_001')
130
- // calls workflow: employee-delete with payload: { id: 'emp_001' }
172
+ ```ts
173
+ // A workflow whose Required Authorization is off — no sign-in needed.
174
+ await app.action.hitSDKFunction('submit_public_enquiry', data, false)
131
175
  ```
132
176
 
133
177
  ---
134
178
 
135
- ## Error Handling
179
+ ## Errors
136
180
 
137
- All errors thrown by the SDK are instances of `OfficelessError`.
181
+ Every failure is thrown as an `OfficelessError` carrying a `type` and the HTTP
182
+ `status`. Branch on `type` — messages are written for people and may change.
138
183
 
139
- ```js
140
- import { OfficelessClient, OfficelessError } from '@officeless/sdk'
184
+ ```ts
185
+ import { OfficelessError } from '@mekari-officeless/sdk'
141
186
 
142
187
  try {
143
- const data = await client.table('employee').list()
144
- } catch (err) {
145
- if (err instanceof OfficelessError) {
146
- console.error(err.message) // human-readable message
147
- console.error(err.statusCode) // HTTP status code (e.g. 404, 500)
148
- console.error(err.response) // raw parsed response body, if available
149
- } else {
150
- throw err // re-throw unexpected errors
188
+ await app.action.hitSDKFunction('calculate_payroll', { month: '2026-08' })
189
+ } catch (error) {
190
+ if (error instanceof OfficelessError) {
191
+ switch (error.type) {
192
+ case 'AuthorizationError':
193
+ // Includes rate limiting — check error.status === 429 to tell it apart.
194
+ break
195
+ case 'ValidationError':
196
+ break
197
+ case 'WorkflowExecutionError':
198
+ break
199
+ case 'InternalServerError':
200
+ break
201
+ }
151
202
  }
152
203
  }
153
204
  ```
154
205
 
155
- `OfficelessError` extends the native `Error` class, so standard `instanceof` checks and stack traces work as expected.
206
+ | `type` | What it means | What to do |
207
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
208
+ | `AuthorizationError` | The key is wrong, revoked or deleted; the domain is not allowed; the session expired; or you are being rate limited (`status` 429). | Check `status`. On 401 with `Session expired`, call `refresh()` or sign the user in again. On 403 for the domain, add it in Studio. On 429, back off and retry. |
209
+ | `ValidationError` | An input did not match what the automation expects, or no function of that name exists in this project and stage. | Fix the input, or check the Function Name and that the workflow uses the SDK Function trigger and is published. |
210
+ | `WorkflowExecutionError` | The automation itself failed partway, or ran past the time limit. | Check the run in Studio's Run History. A run that fails halfway is not rolled back. |
211
+ | `InternalServerError` | Something failed on Officeless's side, or the call never reached it. | Retry. If it persists, contact support. |
212
+
213
+ `status` is `null` when the call never reached the server — offline, DNS failure, or
214
+ a blocked CORS preflight.
156
215
 
157
216
  ---
158
217
 
159
- ## `officeless.config.json` Format
160
-
161
- This file is auto-generated by the AI Porting Agent and placed in the frontend project root. It maps each workflow to its webhook URL.
162
-
163
- ```json
164
- {
165
- "officeless": {
166
- "project_id": "2BfWnyXYk5mz",
167
- "base_url": "https://api.officeless.io",
168
- "api_key": "olk_live_xxxx",
169
- "workflows": {
170
- "employee-list": { "url": "/app/wh/abc123", "method": "post" },
171
- "employee-create": { "url": "/app/wh/def456", "method": "post" },
172
- "employee-update": { "url": "/app/wh/ghi789", "method": "post" },
173
- "employee-delete": { "url": "/app/wh/jkl012", "method": "post" }
174
- }
175
- }
176
- }
218
+ ## Limits
219
+
220
+ | Limit | Value |
221
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
222
+ | Function calls | 100 requests per minute **per project** — shared across every key the project holds, Development and Production together. Exceeding it returns `AuthorizationError` with `status` 429. |
223
+ | Setup calls | Rate limited per key. |
224
+ | Session length | 1 hour. Renew with `refresh()`. |
225
+ | Renewal window | 1 week. |
226
+ | Active sessions | At most 3 per person. A fourth sign-in ends the oldest. |
227
+ | Automation run time | 170 seconds. A longer run returns `WorkflowExecutionError`. |
228
+
229
+ Revoking or deleting a key in Studio ends every session started with it, immediately.
230
+
231
+ ---
232
+
233
+ ## Single-tenant setup
234
+
235
+ `gatewayUrl` points the SDK at a different Officeless deployment. It exists for
236
+ single-tenant customers who run Officeless on their own domain. Most applications
237
+ never set it.
238
+
239
+ ```ts
240
+ const app = await officeless.init({
241
+ sdkKey: 'ofc_sdk_prod_…',
242
+ gatewayUrl: 'https://officeless.your-company.com/v1',
243
+ })
177
244
  ```
178
245
 
179
- Fields:
246
+ **What the SDK builds from it.** Every call is sent to
247
+ `{gatewayUrl}/nocode/sdk/v1/…`. So `gatewayUrl` is the origin **plus whatever prefix
248
+ your gateway is mounted under** — not the origin alone. The shared gateway mounts its
249
+ services under `/v1` and strips that prefix before forwarding, which is why the
250
+ default ends in it. Get this wrong and every call returns the gateway's own HTML 404
251
+ rather than a JSON error, because the request never reaches Officeless at all.
252
+
253
+ > **Set `gatewayUrl` once, in your own deployment configuration.**
254
+ >
255
+ > Never take it from a query parameter, a remotely-fetched config, or any other input
256
+ > an attacker could influence. Your SDK key — and, once a user signs in, their live
257
+ > session tokens — are sent to whatever host this resolves to. A `gatewayUrl` an
258
+ > attacker controls is a `gatewayUrl` that receives those credentials.
259
+ >
260
+ > It cannot be restricted to a Mekari-owned allowlist, because for single-tenant
261
+ > customers any domain may legitimately be correct. That makes it your build's
262
+ > responsibility rather than something the library can enforce.
263
+
264
+ ---
265
+
266
+ ## Security notes
267
+
268
+ **The SDK key is not a secret.** It ships inside your client-side code, and anyone
269
+ can read it with browser developer tools. That is expected. Its protection is the
270
+ allowed-domains list and the rate limit, not secrecy. If a key is misused, revoke it
271
+ in Studio — that takes effect at once and ends every session started with it.
272
+
273
+ **Stored sessions are encrypted, but that is not a defence against XSS.** The access
274
+ and refresh tokens are kept in local storage, encrypted with AES-GCM using a
275
+ non-extractable key held in IndexedDB, so the session survives a page reload. This
276
+ raises the bar against a passive read — browser devtools on a shared machine, a copied
277
+ browser profile. It does **not** protect against a script already running on your
278
+ page: an XSS hole in your application can call the same decryption path the SDK does
279
+ and read the tokens in the clear. Keeping the tokens in memory instead would be no
280
+ better, since a script on the page can reach those too. Treat XSS in your own
281
+ application as the control that matters here.
282
+
283
+ ---
284
+
285
+ ## Environment support
286
+
287
+ This release is a **browser library**. It uses `fetch`, `localStorage`, `IndexedDB`
288
+ and the Web Crypto API, and it relies on the browser setting the `Origin` header —
289
+ that is what the allowed-domains check is based on.
290
+
291
+ Running it on a server (Node, NestJS, Express, a Next.js route handler or server
292
+ component) is **not supported in this release**. A server build needs a different
293
+ security model, and is planned separately.
180
294
 
181
- | Field | Description |
182
- |-------|-------------|
183
- | `project_id` | Unique Officeless project identifier |
184
- | `base_url` | Base API URL for your Officeless instance |
185
- | `api_key` | Secret key for authenticating requests |
186
- | `workflows` | Map of workflow name → `{ url, method }`. `url` may be relative (resolved against `base_url`) or absolute. |
295
+ For frameworks that server-render, keep `init` and every call on the client side:
187
296
 
188
- Commit this file to your repository, but treat `api_key` as a secret — consider loading it from an environment variable in production and excluding the file from version control, or use a build-time substitution step.
297
+ ```tsx
298
+ 'use client'
299
+
300
+ import { useEffect, useState } from 'react'
301
+ import { officeless, type OfficelessApp } from '@mekari-officeless/sdk'
302
+
303
+ export function useOfficeless(sdkKey: string) {
304
+ const [app, setApp] = useState<OfficelessApp | null>(null)
305
+
306
+ useEffect(() => {
307
+ let cancelled = false
308
+ officeless.init({ sdkKey }).then((instance) => {
309
+ if (!cancelled) setApp(instance)
310
+ })
311
+ return () => {
312
+ cancelled = true
313
+ }
314
+ }, [sdkKey])
315
+
316
+ return app
317
+ }
318
+ ```
189
319
 
190
320
  ---
191
321
 
192
322
  ## License
193
323
 
194
- MIT
324
+ Proprietary — © Mekari. Distributed for use with Officeless.