Compare commits

...

8 commits

Author SHA1 Message Date
fenris 243f1bb155 [add] api:session_status 2025-10-02 16:58:27 +02:00
fenris e33fb8bc53 [sty] main [mod] main:session prolongation 2025-10-02 16:58:11 +02:00
fenris 0e8e9ec68b [mod] conf 2025-10-02 16:57:36 +02:00
fenris d8a74f8e37 [sty] api:action:session_begin 2025-10-02 16:57:17 +02:00
fenris 477eeadc14 [fix] tools:plankton 2025-10-02 16:56:42 +02:00
fenris 8b480ecc92 [upd] plankton 2025-10-02 16:56:28 +02:00
fenris 002e057d45 [sty] 2025-10-02 14:30:52 +02:00
fenris 49b9481701 [sty] 2025-10-02 14:30:19 +02:00
11 changed files with 281 additions and 113 deletions

View file

@ -1,11 +1,11 @@
/** /**
* @author fenris * @author fenris
*/ */
declare type int = number; type int = number;
/** /**
* @author fenris * @author fenris
*/ */
declare type float = number; type float = number;
declare var process: any; declare var process: any;
declare var require: any; declare var require: any;
declare class Buffer { declare class Buffer {
@ -22,7 +22,7 @@ declare namespace lib_plankton.base {
/** /**
* @author fenris * @author fenris
*/ */
declare type type_pseudopointer<type_value> = { type type_pseudopointer<type_value> = {
value: type_value; value: type_value;
}; };
/** /**
@ -2315,7 +2315,7 @@ declare namespace lib_plankton.storage.memory {
clear(): Promise<void>; clear(): Promise<void>;
write(key: any, value: any): Promise<boolean>; write(key: any, value: any): Promise<boolean>;
delete(key: any): Promise<void>; delete(key: any): Promise<void>;
read(key: any): Promise<type_item>; read(key: any): Promise<Awaited<type_item>>;
search(term: any): Promise<{ search(term: any): Promise<{
key: string; key: string;
preview: string; preview: string;
@ -3073,22 +3073,25 @@ declare namespace lib_plankton.session {
}; };
/** /**
*/ */
function begin(name: string, options?: { function begin(name: string, { "lifetime": lifetime, "data": data, }?: {
lifetime?: int; lifetime?: int;
data?: any; data?: any;
}): Promise<string>; }): Promise<string>;
/** /**
*/ */
function get(key: string): Promise<type_session>; function get(key: string, { "prolongation": prolongation, }?: {
prolongation?: (null | int);
}): Promise<type_session>;
/** /**
*/ */
function end(key: string): Promise<void>; function end(key: string): Promise<void>;
/** /**
*/ */
function setup(options?: { function setup({ "key_length": key_length, "key_max_attempts": key_max_attempts, "default_lifetime": default_lifetime, "default_prolongation": default_prolongation, "data_chest": data_chest, "clear": clear, }?: {
key_length?: int; key_length?: int;
key_max_attempts?: int; key_max_attempts?: int;
default_lifetime?: int; default_lifetime?: int;
default_prolongation?: (null | int);
data_chest?: lib_plankton.storage.type_chest<string, any, void, string, string>; data_chest?: lib_plankton.storage.type_chest<string, any, void, string, string>;
clear?: boolean; clear?: boolean;
}): Promise<void>; }): Promise<void>;

View file

@ -1486,7 +1486,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
function verb(n) { return function (v) { return step([n, v]); }; } function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) { function step(op) {
if (f) throw new TypeError("Generator is already executing."); if (f) throw new TypeError("Generator is already executing.");
while (_) try { while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value]; if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) { switch (op[0]) {
@ -6814,7 +6814,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
function verb(n) { return function (v) { return step([n, v]); }; } function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) { function step(op) {
if (f) throw new TypeError("Generator is already executing."); if (f) throw new TypeError("Generator is already executing.");
while (_) try { while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value]; if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) { switch (op[0]) {
@ -10266,7 +10266,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
function verb(n) { return function (v) { return step([n, v]); }; } function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) { function step(op) {
if (f) throw new TypeError("Generator is already executing."); if (f) throw new TypeError("Generator is already executing.");
while (_) try { while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value]; if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) { switch (op[0]) {
@ -10325,21 +10325,37 @@ var lib_plankton;
} }
/** /**
*/ */
function begin(name, options) { function session_encode(session) {
if (options === void 0) { options = {}; } return {
"key": session.key,
"name": session.name,
"expiry": Math.floor(session.expiry),
"data": JSON.stringify(session.data)
};
}
/**
*/
function session_decode(session_raw) {
return {
"key": session_raw.key,
"name": session_raw.name,
"expiry": Math.floor(session_raw.expiry),
"data": JSON.parse(session_raw.data)
};
}
/**
*/
function begin(name, _a) {
var _b = _a === void 0 ? {} : _a, _c = _b["lifetime"], lifetime = _c === void 0 ? _conf.default_lifetime : _c, _d = _b["data"], data = _d === void 0 ? null : _d;
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var key, attempts, session_old, error_1, session_2, session_raw; var key, attempts, session_old, error_1, session_2, session_raw;
return __generator(this, function (_a) { return __generator(this, function (_e) {
switch (_a.label) { switch (_e.label) {
case 0: case 0:
options = Object.assign({
"lifetime": _conf.default_lifetime,
"data": null
}, options);
check_conf(); check_conf();
key = null; key = null;
attempts = 0; attempts = 0;
_a.label = 1; _e.label = 1;
case 1: case 1:
if (!true) return [3 /*break*/, 6]; if (!true) return [3 /*break*/, 6];
attempts += 1; attempts += 1;
@ -10349,15 +10365,15 @@ var lib_plankton;
function (x) { return x.join(""); }, function (x) { return x.join(""); },
]); ]);
session_old = null; session_old = null;
_a.label = 2; _e.label = 2;
case 2: case 2:
_a.trys.push([2, 4, , 5]); _e.trys.push([2, 4, , 5]);
return [4 /*yield*/, _conf.data_chest.read(key)]; return [4 /*yield*/, _conf.data_chest.read(key)];
case 3: case 3:
session_old = _a.sent(); session_old = _e.sent();
return [3 /*break*/, 5]; return [3 /*break*/, 5];
case 4: case 4:
error_1 = _a.sent(); error_1 = _e.sent();
session_old = null; session_old = null;
return [3 /*break*/, 5]; return [3 /*break*/, 5];
case 5: case 5:
@ -10383,26 +10399,45 @@ var lib_plankton;
session_2 = { session_2 = {
"key": key, "key": key,
"name": name, "name": name,
"expiry": (lib_plankton.base.get_current_timestamp() + options.lifetime), "expiry": (lib_plankton.base.get_current_timestamp() + lifetime),
"data": options.data "data": data
}; };
session_raw = { session_raw = session_encode(session_2);
"key": session_2.key, /*
"name": session_2.name, lib_plankton.call.timeout(
"expiry": Math.floor(session_2.expiry), () => {
"data": JSON.stringify(session_2.data) lib_plankton.log.info(
}; "session.dropping_due_to_being_expired",
lib_plankton.call.timeout(function () { {
lib_plankton.log.info("session_dropping_due_to_being_expired", { "key": key,
"key": key, "name": name,
"name": name, "lifetime": lifetime,
"lifetime": options.lifetime }
}); );
end(key); end(key);
}, options.lifetime); },
lifetime
);
*/
return [4 /*yield*/, _conf.data_chest.write(key, session_raw)]; return [4 /*yield*/, _conf.data_chest.write(key, session_raw)];
case 8: case 8:
_a.sent(); /*
lib_plankton.call.timeout(
() => {
lib_plankton.log.info(
"session.dropping_due_to_being_expired",
{
"key": key,
"name": name,
"lifetime": lifetime,
}
);
end(key);
},
lifetime
);
*/
_e.sent();
return [2 /*return*/, Promise.resolve(key)]; return [2 /*return*/, Promise.resolve(key)];
} }
}); });
@ -10411,29 +10446,51 @@ var lib_plankton;
session_1.begin = begin; session_1.begin = begin;
/** /**
*/ */
function get(key) { function get(key, _a) {
check_conf(); var _b = _a === void 0 ? {} : _a, _c = _b["prolongation"], prolongation = _c === void 0 ? _conf.default_prolongation : _c;
return (_conf.data_chest.read(key) return __awaiter(this, void 0, void 0, function () {
.then(function (session_raw) { var session_raw, session, now, expiry_old, expiry_new;
var session = { return __generator(this, function (_d) {
"key": session_raw["key"], switch (_d.label) {
"name": session_raw["name"], case 0:
"expiry": session_raw["expiry"], check_conf();
"data": JSON.parse(session_raw["data"]) return [4 /*yield*/, _conf.data_chest.read(key)];
}; case 1:
var now = lib_plankton.base.get_current_timestamp(); session_raw = _d.sent();
if (now > session.expiry) { session = session_decode(session_raw);
lib_plankton.log.info("session_dropping_due_to_being_stale", { now = lib_plankton.base.get_current_timestamp();
"key": session.key, if (!(now <= session.expiry)) return [3 /*break*/, 6];
"name": session.name if (!(prolongation === null)) return [3 /*break*/, 2];
}); return [3 /*break*/, 5];
end(key); case 2:
return Promise.reject(); expiry_old = session.expiry;
} expiry_new = Math.floor(now + prolongation);
else { if (!(expiry_new < session.expiry)) return [3 /*break*/, 3];
return Promise.resolve(session); return [3 /*break*/, 5];
} case 3:
})); lib_plankton.log.info("session.prolongating", {
"key": key,
"now": now,
"prolongation": prolongation,
"expiry_old": expiry_old,
"expiry_new": expiry_new
});
session.expiry = expiry_new;
return [4 /*yield*/, _conf.data_chest.write(key, session_encode(session))];
case 4:
_d.sent();
_d.label = 5;
case 5: return [2 /*return*/, Promise.resolve(session)];
case 6:
lib_plankton.log.info("session.dropping_due_to_being_stale", {
"key": session.key,
"name": session.name
});
end(key);
return [2 /*return*/, Promise.reject()];
}
});
});
} }
session_1.get = get; session_1.get = get;
/** /**
@ -10445,29 +10502,23 @@ var lib_plankton;
session_1.end = end; session_1.end = end;
/** /**
*/ */
function setup(options) { function setup(_a) {
if (options === void 0) { options = {}; } var _b = _a === void 0 ? {} : _a, _c = _b["key_length"], key_length = _c === void 0 ? 16 : _c, _d = _b["key_max_attempts"], key_max_attempts = _d === void 0 ? 3 : _d, _e = _b["default_lifetime"], default_lifetime = _e === void 0 ? 900 : _e, _f = _b["default_prolongation"], default_prolongation = _f === void 0 ? null : _f, _g = _b["data_chest"], data_chest = _g === void 0 ? lib_plankton.storage.memory.implementation_chest({}) : _g, _h = _b["clear"], clear = _h === void 0 ? false : _h;
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) { return __generator(this, function (_j) {
switch (_a.label) { switch (_j.label) {
case 0: case 0:
options = Object.assign({
"key_length": 16,
"key_max_attempts": 3,
"default_lifetime": 900,
"data_chest": lib_plankton.storage.memory.implementation_chest({}),
"clear": false
}, options);
_conf = { _conf = {
"key_length": options.key_length, "key_length": key_length,
"key_max_attempts": options.key_max_attempts, "key_max_attempts": key_max_attempts,
"default_lifetime": options.default_lifetime, "default_lifetime": default_lifetime,
"data_chest": options.data_chest "default_prolongation": default_prolongation,
"data_chest": data_chest
}; };
if (!options.clear) return [3 /*break*/, 2]; if (!clear) return [3 /*break*/, 2];
return [4 /*yield*/, _conf.data_chest.clear()]; return [4 /*yield*/, _conf.data_chest.clear()];
case 1: case 1:
_a.sent(); _j.sent();
return [3 /*break*/, 2]; return [3 /*break*/, 2];
case 2: return [2 /*return*/, Promise.resolve(undefined)]; case 2: return [2 /*return*/, Promise.resolve(undefined)];
} }
@ -15148,7 +15199,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
function verb(n) { return function (v) { return step([n, v]); }; } function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) { function step(op) {
if (f) throw new TypeError("Generator is already executing."); if (f) throw new TypeError("Generator is already executing.");
while (_) try { while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value]; if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) { switch (op[0]) {

View file

@ -65,23 +65,30 @@ namespace _zeitbild.api
}), }),
"restriction": () => restriction_none, "restriction": () => restriction_none,
"execution": () => async ({"input": input}) => { "execution": () => async ({"input": input}) => {
if (input === null) { if (input === null)
{
return Promise.reject(new Error("impossible")); return Promise.reject(new Error("impossible"));
} }
else { else
{
const passed : boolean = await _zeitbild.service.auth_internal.check(input.name, input.password); const passed : boolean = await _zeitbild.service.auth_internal.check(input.name, input.password);
if (! passed) { if (! passed)
return Promise.resolve({ {
"status_code": 403, return Promise.resolve(
"data": null, {
}); "status_code": 403,
"data": null,
}
);
} }
else { else {
const session_key : string = await lib_plankton.session.begin(input.name); const session_key : string = await lib_plankton.session.begin(input.name);
return Promise.resolve({ return Promise.resolve(
"status_code": 201, {
"data": session_key, "status_code": 201,
}); "data": session_key,
}
);
} }
} }
}, },

View file

@ -43,10 +43,12 @@ namespace _zeitbild.api
"execution": async (stuff) => { "execution": async (stuff) => {
const session : {key : string; value : lib_plankton.session.type_session} = await session_from_stuff(stuff); const session : {key : string; value : lib_plankton.session.type_session} = await session_from_stuff(stuff);
await lib_plankton.session.end(session.key); await lib_plankton.session.end(session.key);
return Promise.resolve({ return Promise.resolve(
"status_code": 200, {
"data": null, "status_code": 200,
}); "data": null,
}
);
}, },
} }
); );

View file

@ -80,15 +80,18 @@ namespace _zeitbild.api
(stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null), (stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null),
stuff.query_parameters stuff.query_parameters
); );
if (data.userinfo.name === null) { if (data.userinfo.name === null)
{
return Promise.reject( return Promise.reject(
new Error( new Error(
"IDP did not return user name" "IDP did not return user name"
) )
); );
} }
else { else
try { {
try
{
await _zeitbild.service.user.add( await _zeitbild.service.user.add(
{ {
"name": data.userinfo.name, "name": data.userinfo.name,
@ -103,7 +106,8 @@ namespace _zeitbild.api
} }
); );
} }
catch (error) { catch (error)
{
// do nothing // do nothing
} }
const session_key : string = await lib_plankton.session.begin( const session_key : string = await lib_plankton.session.begin(

View file

@ -0,0 +1,80 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api
{
/**
*/
export function register_session_status(
rest_subject : lib_plankton.rest_http.type_rest
) : void
{
register<
null,
{
logged_in : boolean;
}
>(
rest_subject,
lib_plankton.http.enum_method.get,
"/session/status",
{
"description": "gibt Information über den Nutzer aus",
"output_schema": () => ({
"nullable": false,
"type": "object",
"additionalProperties": false,
"properties": {
"logged_in": {
"nullable": false,
"type": "boolean",
},
},
"required": [
"logged_in",
],
}),
"restriction": restriction_none,
"execution": async (stuff) => {
const user_id : (null | _zeitbild.type_user_id) = await (
session_from_stuff(stuff)
.then(
(session : {key : string; value : lib_plankton.session.type_session;}) => (
_zeitbild.service.user.identify(session.value.name)
.catch(x => Promise.resolve(null))
)
)
.catch(x => Promise.resolve(null))
);
return Promise.resolve(
{
"status_code": 200,
"data": {
"logged_in": (user_id !== null),
}
}
);
}
}
);
}
}

View file

@ -49,6 +49,7 @@ namespace _zeitbild.api
_zeitbild.api.register_session_begin(rest_subject); _zeitbild.api.register_session_begin(rest_subject);
_zeitbild.api.register_session_end(rest_subject); _zeitbild.api.register_session_end(rest_subject);
_zeitbild.api.register_session_oidc(rest_subject); _zeitbild.api.register_session_oidc(rest_subject);
_zeitbild.api.register_session_status(rest_subject);
} }
// user // user
{ {

View file

@ -207,8 +207,13 @@ namespace _zeitbild.conf
"lifetime": { "lifetime": {
"nullable": false, "nullable": false,
"type": "integer", "type": "integer",
"default": 900 "default": 3600,
} },
"prolongation": {
"nullable": true,
"type": "integer",
"default": 300,
},
}, },
"required": [ "required": [
], ],

View file

@ -366,8 +366,10 @@ async function main(
); );
} }
else { else {
switch (args["action"]) { switch (args["action"])
default: { {
default:
{
lib_plankton.log.error( lib_plankton.log.error(
"main_invalid_action", "main_invalid_action",
{ {
@ -376,7 +378,8 @@ async function main(
); );
break; break;
} }
case "conf-schema": { case "conf-schema":
{
process.stdout.write( process.stdout.write(
JSON.stringify( JSON.stringify(
_zeitbild.conf.schema(), _zeitbild.conf.schema(),
@ -388,7 +391,8 @@ async function main(
); );
break; break;
} }
case "conf-expose": { case "conf-expose":
{
process.stdout.write( process.stdout.write(
JSON.stringify( JSON.stringify(
_zeitbild.conf.get(), _zeitbild.conf.get(),
@ -400,7 +404,8 @@ async function main(
); );
break; break;
} }
case "api-doc": { case "api-doc":
{
lib_plankton.log.set_main_logger([]); lib_plankton.log.set_main_logger([]);
const rest_subject : lib_plankton.rest_http.type_rest = _zeitbild.api.make(); const rest_subject : lib_plankton.rest_http.type_rest = _zeitbild.api.make();
process.stdout.write( process.stdout.write(
@ -412,7 +417,8 @@ async function main(
); );
break; break;
} }
case "fill": { case "fill":
{
await data_init( await data_init(
lib_plankton.json.decode( lib_plankton.json.decode(
await lib_plankton.file.read(args.data_path) await lib_plankton.file.read(args.data_path)
@ -421,16 +427,23 @@ async function main(
process.stdout.write("-- done\n"); process.stdout.write("-- done\n");
break; break;
} }
case "serve": { case "serve":
{
// prepare database // prepare database
await _zeitbild.database.check(); await _zeitbild.database.check();
/**
* @todo clear old sessions
*/
await lib_plankton.session.setup( await lib_plankton.session.setup(
{ {
"data_chest": ( "data_chest": (
_zeitbild.conf.get().session_management.in_memory _zeitbild.conf.get().session_management.in_memory
? lib_plankton.storage.memory.implementation_chest<lib_plankton.session.type_session>({}) ?
: lib_plankton.call.convey( lib_plankton.storage.memory.implementation_chest<lib_plankton.session.type_session>({})
:
lib_plankton.call.convey(
lib_plankton.storage.sql_table_common.chest( lib_plankton.storage.sql_table_common.chest(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
@ -452,6 +465,7 @@ async function main(
) )
), ),
"default_lifetime": _zeitbild.conf.get().session_management.lifetime, "default_lifetime": _zeitbild.conf.get().session_management.lifetime,
"default_prolongation": _zeitbild.conf.get().session_management.prolongation,
} }
); );

View file

@ -64,6 +64,7 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/api/actions/session_begin.ts \ ${dir_source}/api/actions/session_begin.ts \
${dir_source}/api/actions/session_oidc.ts \ ${dir_source}/api/actions/session_oidc.ts \
${dir_source}/api/actions/session_end.ts \ ${dir_source}/api/actions/session_end.ts \
${dir_source}/api/actions/session_status.ts \
${dir_source}/api/actions/users.ts \ ${dir_source}/api/actions/users.ts \
${dir_source}/api/actions/user_dav_conf.ts \ ${dir_source}/api/actions/user_dav_conf.ts \
${dir_source}/api/actions/user_dav_token.ts \ ${dir_source}/api/actions/user_dav_token.ts \

View file

@ -43,8 +43,8 @@ mkdir -p ${dir}
mkdir /tmp/sandbox -p mkdir /tmp/sandbox -p
cd /tmp/sandbox cd /tmp/sandbox
ptk fetch node ${modules} ptk fetch node ${modules}
schwamm --include=/tmp/sandboxplankton.swm.json --output=dump:logic-decl > ${dir}/plankton.d.ts schwamm --include=/tmp/sandbox/plankton.swm.json --output=dump:logic-decl > ${dir}/plankton.d.ts
schwamm --include=/tmp/sandboxplankton.swm.json --output=dump:logic-impl > ${dir}/plankton.js schwamm --include=/tmp/sandbox/plankton.swm.json --output=dump:logic-impl > ${dir}/plankton.js
exit exit
mkdir -p ${dir} mkdir -p ${dir}